77 Commits

Author SHA1 Message Date
syntaxbullet
222f32d98f Improve panel layout overflow on small screens
All checks were successful
CI / Deploy / test (push) Successful in 1m15s
CI / Deploy / deploy (push) Successful in 1m4s
- Prevent horizontal overflow in the main layout
- Stack game room controls vertically on narrow viewports
- Truncate long room and user labels to keep cards stable
2026-04-10 12:13:03 +02:00
syntaxbullet
454ded8b26 fix: remove pico files.
All checks were successful
CI / Deploy / test (push) Successful in 1m12s
CI / Deploy / deploy (push) Successful in 1m6s
2026-04-10 12:02:37 +02:00
syntaxbullet
9e85ba1fa4 Refresh waiting room cleanup on activity
Some checks failed
CI / Deploy / test (push) Successful in 1m15s
CI / Deploy / deploy (push) Has been cancelled
- Extend waiting rooms while players or spectators are active
- Make cleanup time configurable for tests and defaults
- Tweak lobby layout for smaller screens
2026-04-10 12:00:59 +02:00
syntaxbullet
2fb8d559a6 Streamline game lobby copy and room facts
All checks were successful
CI / Deploy / test (push) Successful in 1m15s
CI / Deploy / deploy (push) Successful in 1m4s
- Tighten lobby messaging and empty-state copy
- Remove the obsolete "What Changed" sidebar
- Build room facts inline instead of with `useMemo`
2026-04-10 11:49:36 +02:00
syntaxbullet
b0a103d8ce Show default chess control detail in room summary
All checks were successful
CI / Deploy / test (push) Successful in 1m12s
CI / Deploy / deploy (push) Successful in 1m5s
- Use the selected chess time control detail when available
- Fall back to a balanced default description when no match is found
2026-04-10 11:36:32 +02:00
syntaxbullet
cb056e010f Redesign game lobby and room creation flow
Some checks failed
CI / Deploy / test (push) Failing after 48s
CI / Deploy / deploy (push) Has been skipped
- Split chess and blackjack setup into guided creation steps
- Add chess time control presets and reusable lobby room metrics
- Improve room filtering, ordering, and live connection state
2026-04-10 11:34:12 +02:00
syntaxbullet
de15cb4206 Show explicit blackjack settlements across the stack
All checks were successful
CI / Deploy / test (push) Successful in 1m18s
CI / Deploy / deploy (push) Successful in 1m4s
- Replace round payout multipliers with per-player settlement amounts
- Update blackjack panel to display wager, payout, and net results
2026-04-10 11:03:58 +02:00
syntaxbullet
f796cac6be Add chess premoves and time control metadata
All checks were successful
CI / Deploy / test (push) Successful in 1m15s
CI / Deploy / deploy (push) Successful in 1m11s
- pass chess room time control to the client
- add premove handling and richer chess board UI
- update join result typing for room options
2026-04-10 10:19:33 +02:00
syntaxbullet
31580df919 update example readme with session secret
All checks were successful
CI / Deploy / test (push) Successful in 2m18s
CI / Deploy / deploy (push) Successful in 1m9s
2026-04-09 22:15:58 +02:00
syntaxbullet
9a17209db2 Add CI and deploy workflow
Some checks failed
CI / Deploy / test (push) Successful in 2m20s
CI / Deploy / deploy (push) Failing after 5s
- Run typecheck, panel build, and integration tests in Gitea
- Deploy main branch to VPS over SSH and verify the health endpoint
2026-04-09 22:04:23 +02:00
syntaxbullet
04656790d2 Use isolated test runner in deploy workflow
All checks were successful
Deploy to Production / test (push) Successful in 34s
- Update deploy CI to invoke `shared/scripts/test-isolated.sh`
- Refresh the inline note for the test environment setup
2026-04-09 21:46:53 +02:00
syntaxbullet
25a0bd3431 Sign panel sessions and isolate test runs
Some checks failed
Deploy to Production / test (push) Failing after 29s
- Replace in-memory auth sessions with signed cookies and signed OAuth state
- Add auth route coverage and update panel/web server wiring
- Switch test script to per-file Bun processes and clean up type checks
2026-04-09 21:44:05 +02:00
syntaxbullet
6abbd4652a Refresh repository documentation
Some checks failed
Deploy to Production / test (push) Failing after 33s
- Rewrite AGENTS and README files to match the current app layout
- Document API routes, trivia UI, and the active panel design language
2026-04-09 21:10:10 +02:00
syntaxbullet
8369d10bab Add auth checks for user routes and dashboard state
Some checks failed
Deploy to Production / test (push) Failing after 33s
- tighten route authorization and schema handling
- update user route tests and server coverage
- refresh player dashboard behavior
2026-04-09 20:42:32 +02:00
syntaxbullet
bdfe0d1594 chore: fix blackjack UI overflow and table sizing
Some checks failed
Deploy to Production / test (push) Failing after 34s
- Remove max-w-4xl constraint to allow single-player tables to expand
- Add responsive container sizing: 95vw on mobile, up to 6xl on large screens
- Add horizontal scrolling for split hands with overflow-x-auto
- Increase spacing between split hands on larger screens (gap-1.5 -> sm:gap-2)
- Add padding to container for better spacing on small screens
2026-04-06 15:18:15 +02:00
syntaxbullet
034f2ead1c fix: correct blackjack PnL calculation and enhance UI
Some checks failed
Deploy to Production / test (push) Failing after 35s
- Fix incorrect PnL calculations where multipliers were treated as amounts
- Add proper net profit calculation: (multiplier × bet) - bet
- Update UI with gradient backgrounds, icons, and improved animations
- Add slide-in and pulse-slow keyframe animations
- Enhance dealer area with status-based styling
- Improve action buttons with colored gradients and hover effects
- Revise round result banner with better visual hierarchy
2026-04-06 15:11:47 +02:00
syntaxbullet
06c3891045 fix: Correct PnL calculation by using betAmount in net profit computation
Some checks failed
Deploy to Production / test (push) Failing after 34s
- Add betAmount field to BlackjackState to track the base bet
- Fix finishPlayerTurns: multiply hand.bet by state.betAmount for actual money bets
- Fix GameServer: roundPayouts are already gross payouts (not multipliers)
- Update cumulative PnL calculation to correctly subtract actual bet amount
- Add betAmount support to riggedState test helper
2026-04-06 14:50:40 +02:00
syntaxbullet
f09cbe6939 fix: Restore myBetPlaced variable that was incorrectly removed
Some checks failed
Deploy to Production / test (push) Failing after 34s
- Re-added myBetPlaced variable which is used for betting phase UI rendering
2026-04-06 14:44:24 +02:00
syntaxbullet
cd9e1e7242 fix: Correct PnL calculation to show net profit instead of gross payout
Some checks failed
Deploy to Production / test (push) Failing after 36s
- GameServer.ts: Calculate netProfit = grossPayout - betAmount and send as 'net'
  instead of sending gross payout labeled as net
- BlackjackGame.tsx: Fix PnL calculations to use net profit correctly
  - Hand win/blackjack now shows net profit (payout minus bet)
  - Lose correctly shows negative bet amount
  - Round result banner displays roundNet (net profit) with appropriate colors
- Remove dead code (myPnl variable that was calculated but never used)
- Update color coding: green for profit, red for loss, blue for zero balance
2026-04-06 14:42:18 +02:00
syntaxbullet
966bad98d3 Add cumulative PnL tracking to Blackjack game
Some checks failed
Deploy to Production / test (push) Failing after 32s
- Added cumulativePnl field to PlayerSeat and PlayerSeatView types
- Added myCumulativePnl to PlayerView for UI display
- Track net profit/loss across rounds in the game state
- Update round result banner to show both round net and total balance
- Add player seat PnL indicator with color coding (green/red)
- Preserve cumulativePnl when players stay seated through rounds
- Initialize new players with cumulativePnl = 0
- Added comprehensive tests for cumulative PnL tracking
2026-04-06 14:31:58 +02:00
syntaxbullet
2b89fb7ede docs: rename CLAUDE.md to AGENTS.md across the project
Some checks failed
Deploy to Production / test (push) Failing after 34s
2026-04-06 14:18:56 +02:00
syntaxbullet
0fc88323ea fix(panel): add GAME_BET and GAME_WIN to transaction type config
Some checks failed
Deploy to Production / test (push) Failing after 30s
Missing TYPE_CONFIG entries caused both transaction types to fall back to
sign: "+", making bet deductions appear as credits in the admin panel.
The actual economy values were correct (negative for bets).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:59:41 +02:00
syntaxbullet
96eba8270c fix(games): skip upfront bet deduction for per-round betting games
Some checks failed
Deploy to Production / test (push) Failing after 35s
Games with getActionCost (like blackjack) handle bet deductions per-round
via place_bet actions. The old deductBetsAndStart was also charging at game
start, causing double-deduction: wins netted zero and losses doubled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:54:21 +02:00
syntaxbullet
a36c05994c feat(games): refactor blackjack for continuous play, split/double, and table UI
Some checks failed
Deploy to Production / test (push) Failing after 32s
Transform blackjack from single-round to continuous-play table sessions with
round lifecycle (betting → playing → resolved → betting), split/double down
actions, per-hand bet tracking, leave/join table mid-session, and a responsive
felt-style table UI with arc-positioned player seats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:41:49 +02:00
syntaxbullet
ef78a85b9c feat(games): implement blackjack game plugin with manual start and custom payouts
Some checks failed
Deploy to Production / test (push) Failing after 39s
Adds a full blackjack game with dealer AI, hit/stand/double-down actions,
and per-player payout multipliers (house-edge model). Extends the game
framework with manualStart support and a START_GAME WebSocket message so
hosts can begin when ready. Generalizes bet settlement transaction
descriptions from chess-specific to game-agnostic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:48:25 +02:00
syntaxbullet
f368da9e73 feat(games): add solo mode to room creation and AU currency betting
Some checks failed
Deploy to Production / test (push) Failing after 31s
Solo mode is now a toggle in the chess room creation modal, available
to all users instead of admin-only. Betting lets players wager AU on
games with preset amounts, async deduction on game start, and automatic
payout/refund on game end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:09:03 +02:00
syntaxbullet
4f89ed3082 fix(games): stop spectator broadcast from overwriting player state
Some checks failed
Deploy to Production / test (push) Failing after 36s
Players subscribe to the room pub/sub channel and also receive direct
GAME_STATE messages. The GAME_STARTED and GAME_UPDATE broadcasts carry
the spectator view (no myColor/legalMoves), and were blindly overwriting
gameState — making isPlayerView() return false and disabling all
interaction. Now these broadcast handlers only update gameState for
spectators; players rely exclusively on the direct GAME_STATE message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:30:45 +02:00
syntaxbullet
0d8152914a refactor(games): inline SVG pieces instead of <img> elements
Some checks failed
Deploy to Production / test (push) Failing after 30s
Replace external SVG files loaded via <img> tags with inline React SVG
components. The <img> approach was a DOM-level problem — replaced elements
sit outside React's tree and interfere with dnd-kit's pointer event
pipeline. Inline SVGs are native JSX nodes that participate correctly
in event bubbling, matching how react-chessboard's default pieces work.

Removes panel/public/pieces/ (12 SVG files) in favor of a single
pieces.tsx module with the same cburnett artwork as JSX.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:27:20 +02:00
syntaxbullet
12809623c1 fix(games): fix chess piece interaction and solo mode
Some checks failed
Deploy to Production / test (push) Failing after 32s
Two bugs fixed:
- Piece <img> elements were intercepting pointer events, preventing
  dnd-kit drag handlers and square click handlers from firing. Added
  pointerEvents: "none" so events pass through to the board framework.
- Solo test mode (fillRoom with duplicate player IDs) always resolved
  to "white" in colorOfPlayer, making black moves impossible. Now
  detects duplicate IDs and returns the current turn's color instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:23:16 +02:00
syntaxbullet
a29bb63a1d feat(games): implement chess game plugin with full UI
Some checks failed
Deploy to Production / test (push) Failing after 32s
Add chess as the first game plugin using the existing multiplayer framework.
Server-side game logic uses chess.js with server-authoritative clock management.
Client uses react-chessboard v5 with cburnett piece set, drag-and-drop + click-to-move,
configurable time controls (bullet/blitz/rapid/classical/none), draw offers,
resignation, and timeout detection. Extends the game framework with room creation
options to support per-game configuration. Includes 57 tests covering all code paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:59:26 +02:00
syntaxbullet
9e95194627 fix(panel): double negative sign on transaction amounts
Some checks failed
Deploy to Production / test (push) Failing after 34s
formatAmount now strips the sign since the renderer already prepends
+/- based on TYPE_CONFIG. Raw DB values like "-180" were getting a
second "-" prefix, showing as "--180".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:28:40 +02:00
syntaxbullet
451fb206a6 fix(panel): transaction types showing wrong sign for trivia, trades, etc.
Some checks failed
Deploy to Production / test (push) Failing after 32s
TYPE_CONFIG only had 6 of 14 transaction types. Missing types fell back
to sign: null which rendered as negative. Added all TransactionType enum
values and derived the filter dropdown from TYPE_CONFIG keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:25:25 +02:00
syntaxbullet
e3c49effdb feat(panel): replace all placeholder pages with real admin views
Some checks failed
Deploy to Production / test (push) Failing after 38s
- Classes: full CRUD with list + detail panel
- Quests: CRUD with search, trigger events and reward fields
- Lootdrops: stat cards, spawn form, filter tabs, cancel action
- Moderation: case list with filters, detail panel, create + resolve
- Transactions: color-coded amounts, type/user filters, pagination
- Rename player dashboard currency label from Gold to AU (Astral Units)
- Remove unused placeholders map from App.tsx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:19:23 +02:00
syntaxbullet
5c40249a18 feat(panel): implement player leaderboards page
Some checks failed
Deploy to Production / test (push) Failing after 31s
Replaces the placeholder with a real leaderboard view showing top 10
players by level, wealth, and net worth. Reuses the existing /api/stats
endpoint which players already have access to.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:02:43 +02:00
syntaxbullet
b645f55f57 fix(panel): player inventory not loading due to API response mismatch
Some checks failed
Deploy to Production / test (push) Failing after 32s
Frontend expected { items } but API returns { inventory } with nested
item objects. Fixes response key, aligns InventoryEntry type to actual
API shape, and separates error handling so a failed inventory fetch
shows an error instead of silently displaying "No items yet".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:55:25 +02:00
syntaxbullet
838fbe1b50 feat(panel): group sidebar nav so admins see both admin and player views
Some checks failed
Deploy to Production / test (push) Failing after 34s
Introduces NavGroup structure with labeled sections in the sidebar.
Admins see "Administration" and "Player" groups; players see a flat
list unchanged. Extracts SidebarNavItem, SidebarNavSection, and
SidebarUserProfile components from the monolithic sidebarContent blob.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:45:04 +02:00
syntaxbullet
94d259e92a fix(panel): guild settings not pre-filling from database
Some checks failed
Deploy to Production / test (push) Failing after 33s
Guild draft was initialized with defaults before the API response
arrived, then never updated because the !guildDraft guard prevented
overwriting. Gate initialization on !loading so saved values are used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:28:40 +02:00
syntaxbullet
56db5bc998 refactor(games): rework room lifecycle events and remove chess plugin
Consolidate room leave/delete event handling into RoomManager emitter,
remove redundant PLAYER_LEFT publishes from GameServer, and delete the
chess game plugin (board, types, tests) in favor of the new plugin
architecture. Add per-module CLAUDE.md files for leveling, guild-settings,
feature-flags, db, api, and panel to improve agent navigability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:19:51 +02:00
syntaxbullet
ebac1ad6cc fix(chess): migrate ChessBoard to react-chessboard v5 API
Some checks failed
Deploy to Production / test (push) Failing after 33s
react-chessboard v5 moved all props into an `options` object and
renamed several callbacks/style props. The v4-style props were silently
ignored, causing pieces to snap back, no legal-move highlights, and no
WS events on drop. Also adds a custom promotion dialog since v5 removed
the built-in one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:26:24 +02:00
syntaxbullet
abca1922f2 chore: change styling
Some checks failed
Deploy to Production / test (push) Failing after 34s
2026-04-02 19:05:36 +02:00
syntaxbullet
e0dcfe6abe fix: (chess) new styling
Some checks failed
Deploy to Production / test (push) Failing after 34s
2026-04-02 17:28:50 +02:00
syntaxbullet
132f92d2d9 fix(chess): optimistic moves and forfeit UI feedback
Some checks failed
Deploy to Production / test (push) Failing after 31s
- Add localFen/localFenRef in ChessBoard for optimistic piece placement,
  preventing snap-back while awaiting server confirmation
- Sync localFen from server state on each chess.fen update
- Guard GAME_STATE handler in useGameRoom from overwriting a finished
  roomStatus, fixing the race where GAME_ENDED (pub/sub) arrives before
  GAME_STATE (direct ws.send)
- Reset confirmForfeit immediately on forfeit dispatch for instant UI feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 17:06:58 +02:00
syntaxbullet
70a149ab82 refactor(games): overhaul WS game system with improved UX and solo test support
Some checks failed
Deploy to Production / test (push) Failing after 35s
Backend:
- Fix session never being attached to ws.data at upgrade time
- Add GameServer class: connection registry, per-connection room tracking,
  automatic room cleanup on disconnect via ws.data.rooms
- Replace ws-handler.ts with typed event-driven architecture using mitt
- Remove redundant subscription tracking from RoomManager
- Add JOIN_RESULT with player/spectator lists replacing error-as-control-flow
- Add SESSION_REPLACED for multi-tab same-account detection
- Add FILL_ROOM command for admin solo testing (fills empty slots with host)
- Fix dual-schema routing; remove game types from WsMessageSchema
- Per-player personalized views sent directly after each action

Chess plugin:
- Allow same-player (solo) mode: skip color/turn ownership checks
- Fix forfeit and disconnect handling in solo mode (winner: null)

Frontend:
- Click-to-move with legal move dots and last-move highlight
- Auto-scroll move history, forfeit confirmation, turn-reactive board border
- JOIN_RESULT initialises player/spectator lists immediately on join
- Contextual connecting state, player slot cards in waiting room
- Copy-invite button with Copied! flash, Back to Lobby CTA on finish
- Session-replaced warning banner with Rejoin here action
- Lobby passes preferAs intent through route state
- Admin waiting room shows Start Solo Test button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 16:41:13 +02:00
syntaxbullet
26a0e532f6 fix(chess): prevent duplicate players and fix move detection
Some checks failed
Deploy to Production / test (push) Failing after 38s
- Prevent same player from joining as both white and black
- Add validation to reject duplicate players in RoomManager
- Fix spectator status not resetting when joining as player
- Use ref to track latest chess state in ChessBoard for accurate move validation
2026-04-02 15:36:05 +02:00
syntaxbullet
e521d3086f fix(chess): admin users and move registration
Some checks failed
Deploy to Production / test (push) Failing after 29s
- Add role field to JOIN_ROOM message schema
- Allow admin users to join rooms and be added as players
- Update panel to pass user role when joining game rooms
- Fix chess move coordinates in tests (algebraic notation)
- Ensure admin users can make moves for both sides
2026-04-02 15:27:56 +02:00
syntaxbullet
9c4da51cfb fix(chess): send game updates to move sender + responsive mobile redesign
Some checks failed
Deploy to Production / test (push) Failing after 32s
Bun's ws.publish() excludes the sender, so the player making a move never
received the GAME_UPDATE with the new FEN — causing pieces to snap back.
Added ctx.send() alongside ctx.publish() for GAME_UPDATE and GAME_ENDED.

Also redesigned the panel for mobile: hamburger drawer sidebar, responsive
chess board sizing via ResizeObserver, stacked layouts on small screens,
and touch-friendly modals/controls across lobby and game pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:58:40 +02:00
syntaxbullet
24211dca14 fix(chess): robust client-side validation and admin self-play support
Some checks failed
Deploy to Production / test (push) Failing after 35s
Wrap chess.js move validation in try-catch for invalid moves. Fix admin
self-play by detecting when both players share the same ID and allowing
either color's pieces to be dragged on the current turn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:27:33 +02:00
syntaxbullet
87b66cd65d feat(games): allow admins to play against themselves
Some checks failed
Deploy to Production / test (push) Failing after 35s
Skip the duplicate-player check when the joining user has the admin
role, so admins can test games solo by joining their own room as both
players.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:24:23 +02:00
syntaxbullet
0dadc82f84 feat(chess): replace custom engine with chess.js and react-chessboard
Some checks failed
Deploy to Production / test (push) Failing after 39s
Swap the custom move validation and Unicode piece grid for chess.js
(full rules engine with check/checkmate/castling/en passant/promotion)
and react-chessboard (drag-and-drop SVG board). Board styled to match
the purple dark theme and auto-orients to the player's color.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:22:27 +02:00
syntaxbullet
5527981fff fix(panel): keep WebSocket alive across route transitions
Some checks failed
Deploy to Production / test (push) Failing after 33s
Disconnecting and reconnecting the WebSocket on every route change caused
a race condition: the old socket's async onclose handler would null out
globalWs after the new socket was created, causing JOIN_ROOM messages to
be silently dropped. The WebSocket is a global singleton — keep it alive
and let the built-in reconnection logic handle actual disconnects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:03:35 +02:00
syntaxbullet
9f105ada5e fix(games): send room state directly to joining client
Some checks failed
Deploy to Production / test (push) Failing after 35s
Bun's ws.publish() excludes the sender, so the joining client never
received the PLAYER_JOINED message and stayed stuck on the connecting
spinner. Now sends the message directly to the joiner via ctx.send
in addition to publishing to other room subscribers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:56:24 +02:00
syntaxbullet
cac9fae142 fix(games): allow room creator to rejoin without error
Some checks failed
Deploy to Production / test (push) Failing after 34s
The room creator was added as a player during createRoom, but when
GameRoom mounted it sent JOIN_ROOM which failed with "Already in room",
leaving the UI stuck on the loading spinner. Now treats duplicate joins
as successful rejoins, which also fixes reconnection after page refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:52:59 +02:00
syntaxbullet
b832723d6b fix(panel): show login screen instead of enrollment page for unauthenticated users
Some checks failed
Deploy to Production / test (push) Failing after 28s
/auth/me was returning enrolled: false for all unauthenticated requests,
causing the frontend to show the NotEnrolled page before login. Enrollment
is already enforced server-side during the OAuth callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:45:37 +02:00
syntaxbullet
0c3b289ba0 feat(panel): implement GameLobby and GameRoom pages
Some checks failed
Deploy to Production / test (push) Failing after 34s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:31:12 +02:00
syntaxbullet
f4b36a745e feat(panel): implement player dashboard with stats and inventory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:31:09 +02:00
syntaxbullet
3b53c9cb5f feat(games): register chess plugin on server startup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:30:57 +02:00
syntaxbullet
3bdb720e4a feat(panel): migrate to React Router, role-based layout and routing
Replace useState-based page switching with react-router-dom Routes.
Layout now renders admin or player nav items based on user.role.
Add stub pages for PlayerDashboard, GameLobby, and GameRoom.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:29:14 +02:00
syntaxbullet
f290eeeb8a feat(panel): add game UI registry and chess board component
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:29:13 +02:00
syntaxbullet
4b3f6590cc feat(panel): add useGameRoom hook for per-room game state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:28:42 +02:00
syntaxbullet
069c0b93ef feat(games): integrate game WS handler into server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:28:41 +02:00
syntaxbullet
33a1848096 feat(games): implement RoomManager with room lifecycle and tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:26:26 +02:00
syntaxbullet
55df982a0b feat(games): implement chess plugin with tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:25:54 +02:00
syntaxbullet
eb7dfaf6f5 feat(panel): add shared useWebSocket hook with reconnection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:25:52 +02:00
syntaxbullet
aa145592c5 feat(panel): add react-router-dom, update auth hook with roles, add NotEnrolled page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:23:39 +02:00
syntaxbullet
37fa5fc3c8 feat(auth): add enrollment check, role-based sessions, and player access
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:23:35 +02:00
syntaxbullet
db10ebe220 feat(games): add room types and game WS message schemas
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:23:11 +02:00
syntaxbullet
a5478dce2b feat(games): add GamePlugin interface and registry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:22:48 +02:00
syntaxbullet
29b6153777 docs: add web games platform implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:58:07 +02:00
syntaxbullet
d3e83bac66 docs: remove ambiguous options field from CREATE_ROOM message
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:49:42 +02:00
syntaxbullet
40ae93f68b docs: add web games platform design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:49:21 +02:00
syntaxbullet
1e978dff58 refactor(panel): extract page sub-components from mega-files
Some checks failed
Deploy to Production / test (push) Failing after 33s
Split ItemStudio (1863->388), Settings (1445->355), and Users
(1062->164) into focused sub-components under pages/components/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:39 +02:00
syntaxbullet
3c256ba0b2 refactor: centralize custom interaction IDs into constants
Replace all hardcoded custom ID strings with module-level constants.
Each module now has *_CUSTOM_IDS in its types file, using functions
for dynamic IDs and PREFIX for startsWith matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:35 +02:00
syntaxbullet
70d59a091a docs: fix drift in docs/main.md
Fix web/ -> api/, add missing panel/modules/graphics sections,
expand module and utility listings to match actual codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:26 +02:00
syntaxbullet
9569972cd6 docs: document interaction routing flow in CLAUDE.md
Add routing table mapping custom ID prefixes to handler files and
describe the ComponentInteractionHandler dispatch mechanism.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:22 +02:00
syntaxbullet
5bd390b4ee docs: add JSDoc to service public methods
One-line JSDoc on 82 methods across 11 service files for quick
scanning without reading full implementations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:18 +02:00
syntaxbullet
5f8819bb46 docs: add subdirectory CLAUDE.md files for key domain modules
Provide non-obvious business rules and constraints for economy,
inventory, quest, moderation, trade, and trivia modules to reduce
context-gathering overhead for AI tools.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:36:12 +02:00
syntaxbullet
b8cf136ff7 fix: fix wording for non-purchasable items
Some checks failed
Deploy to Production / test (push) Failing after 33s
2026-03-31 16:56:41 +02:00
205 changed files with 32929 additions and 8651 deletions

View File

@@ -31,3 +31,4 @@ PANEL_BASE_URL=http://localhost:3000
# Use a non-root user (see shared/scripts/setup-server.sh)
VPS_USER=deploy
VPS_HOST=your-vps-ip
SESSION_SECRET=change-me-to-a-random-string

View File

@@ -0,0 +1,132 @@
name: CI / Deploy
on:
push:
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: aurora_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
run: |
curl -fsSL https://bun.sh/install | bash
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
- name: Install Dependencies
run: bun install --frozen-lockfile
- name: Create Config File
run: |
mkdir -p shared/config
cat <<EOF > shared/config/config.json
{
"leveling": { "base": 100, "exponent": 2.5, "chat": { "cooldownMs": 60000, "minXp": 15, "maxXp": 25 } },
"economy": {
"daily": { "amount": "100", "streakBonus": "10", "weeklyBonus": "50", "cooldownMs": 86400000 },
"transfers": { "allowSelfTransfer": false, "minAmount": "1" },
"exam": { "multMin": 0.05, "multMax": 0.03 }
},
"inventory": { "maxStackSize": "99", "maxSlots": 50 },
"commands": {},
"lootdrop": {
"activityWindowMs": 120000, "minMessages": 1, "spawnChance": 1, "cooldownMs": 3000,
"reward": { "min": 40, "max": 150, "currency": "Astral Units" }
},
"studentRole": "123", "visitorRole": "456", "colorRoles": [],
"moderation": {
"prune": { "maxAmount": 100, "confirmThreshold": 50, "batchSize": 100, "batchDelayMs": 1000 },
"cases": { "dmOnWarn": false }
},
"trivia": {
"entryFee": "50", "rewardMultiplier": 1.5, "timeoutSeconds": 30, "cooldownMs": 60000,
"categories": [], "difficulty": "random"
},
"system": {}
}
EOF
- name: Typecheck
run: bunx tsc --noEmit
- name: Build Panel
run: bun run panel:build
- name: Setup Test Database
run: bun run db:push:local
env:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/aurora_test
DISCORD_BOT_TOKEN: test_token
DISCORD_CLIENT_ID: "123"
DISCORD_GUILD_ID: "123"
- name: Run Tests
run: |
cat <<EOF > .env.test
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/aurora_test"
DISCORD_BOT_TOKEN="test_token"
DISCORD_CLIENT_ID="123456789"
DISCORD_GUILD_ID="123456789"
DISCORD_CLIENT_SECRET="test-client-secret"
SESSION_SECRET="test-session-secret"
ADMIN_TOKEN="admin_token_123"
LOG_LEVEL="error"
EOF
bash shared/scripts/test-isolated.sh --integration
env:
NODE_ENV: test
deploy:
needs: test
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Configure SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts
- name: Deploy on VPS
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
VPS_PROJECT_PATH: ${{ secrets.VPS_PROJECT_PATH }}
run: |
set -euo pipefail
REMOTE_DIR="${VPS_PROJECT_PATH:-~/Aurora}"
ssh -o BatchMode=yes "$VPS_USER@$VPS_HOST" "cd $REMOTE_DIR && bash shared/scripts/deploy.sh"
- name: Post-deploy Health Check
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
VPS_PROJECT_PATH: ${{ secrets.VPS_PROJECT_PATH }}
run: |
set -euo pipefail
REMOTE_DIR="${VPS_PROJECT_PATH:-~/Aurora}"
ssh -o BatchMode=yes "$VPS_USER@$VPS_HOST" "cd $REMOTE_DIR && curl -fsS http://127.0.0.1:3000/api/health >/dev/null"

View File

@@ -86,7 +86,7 @@ jobs:
- name: Run Tests
run: |
# Create .env.test for test-sequential.sh / bun test
# Create .env.test for the isolated test runner / bun test
cat <<EOF > .env.test
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/aurora_test"
DISCORD_BOT_TOKEN="test_token"
@@ -95,6 +95,6 @@ jobs:
ADMIN_TOKEN="admin_token_123"
LOG_LEVEL="error"
EOF
bash shared/scripts/test-sequential.sh --integration
bash shared/scripts/test-isolated.sh --integration
env:
NODE_ENV: test

312
AGENTS.md
View File

@@ -1,257 +1,135 @@
# AGENTS.md - AI Coding Agent Guidelines
# AGENTS.md
## Project Overview
This file documents the current implementation shape of the Aurora repository.
AuroraBot is a Discord bot with a REST API built using Bun, Discord.js, and PostgreSQL with Drizzle ORM.
## Build/Lint/Test Commands
## Commands
```bash
# Development
bun --watch bot/index.ts # Run bot + API server with hot reload
# App
bun run dev # bot + API in one Bun process with watch mode
docker compose up # app + db
docker compose up app # app only
docker compose up db # database only
# Testing
bun test # Run all tests
bun test path/to/file.test.ts # Run single test file
bun test --watch # Watch mode
bun test shared/modules/economy # Run tests in directory
bun test # Bun's native runner
bun run test # repo test wrapper script
bun run test:ci # include CI/integration path
# Database
bun run generate # Generate Drizzle migrations (Docker)
bun run migrate # Run migrations (Docker)
bun run db:push # Push schema changes (Docker)
bun run db:push:local # Push schema changes (local)
bun run db:studio # Open Drizzle Studio
bun run db:push # drizzle-kit push via Docker
bun run db:push:local # drizzle-kit push locally
bun run db:generate # drizzle-kit generate via Docker
bun run db:migrate # drizzle-kit migrate via Docker
bun run db:studio # local Drizzle Studio on :4983
# Docker (recommended for local dev)
docker compose up # Start bot, API, and database
docker compose up app # Start just the app (bot + API)
docker compose up db # Start just the database
# Panel
bun run panel:dev # Vite dev server on :5173
bun run panel:build # build panel/dist
```
## Project Structure
## Architecture
```
bot/ # Discord bot
├── commands/ # Slash commands by category
├── events/ # Discord event handlers
├── lib/ # Bot core (BotClient, handlers, loaders)
├── modules/ # Feature modules (views, interactions)
└── graphics/ # Canvas image generation
Aurora is a single-process Bun application:
shared/ # Shared between bot and web
├── db/ # Database schema and migrations
├── lib/ # Utils, config, errors, types
└── modules/ # Domain services (economy, user, etc.)
- `bot/index.ts` boots shared config, registers domain listeners, starts the API server, then logs into Discord.
- `api/src/server.ts` hosts REST routes, WebSocket traffic, and built panel assets.
- `shared/modules/*` contains the business logic used by both the bot and the API.
- `shared/games/*` contains reusable game plugins; `api/src/games/*` runs rooms and WebSocket orchestration.
web/ # API server
└── src/routes/ # API route handlers
Current high-level layout:
```text
bot/ Discord commands, events, views, interactions
api/ Bun HTTP + WebSocket server
panel/ React dashboard
shared/db/ Drizzle client and schema
shared/lib/ config, env, errors, logger, events, constants
shared/modules/ domain services
shared/games/ game plugins shared by API and panel
```
## Import Conventions
## Import conventions
Use path aliases defined in tsconfig.json:
Use path aliases from the repo `tsconfig.json`:
```typescript
// External packages first
import { SlashCommandBuilder } from "discord.js";
import { eq } from "drizzle-orm";
- `@/*` -> `bot/*`
- `@commands/*` -> `bot/commands/*`
- `@db/*` -> `shared/db/*`
- `@lib/*` -> `bot/lib/*`
- `@modules/*` -> `bot/modules/*`
- `@shared/*` -> `shared/*`
// Path aliases second
import { economyService } from "@shared/modules/economy/economy.service";
import { UserError } from "@shared/lib/errors";
import { users } from "@db/schema";
import { createErrorEmbed } from "@lib/embeds";
import { handleTradeInteraction } from "@modules/trade/trade.interaction";
Import order in the repo is generally:
// Relative imports last
import { localHelper } from "./helper";
```
1. external packages
2. aliases
3. relative imports
**Available Aliases:**
## File patterns
- `@/*` - bot/
- `@shared/*` - shared/
- `@db/*` - shared/db/
- `@lib/*` - bot/lib/
- `@modules/*` - bot/modules/
- `@commands/*` - bot/commands/
- `*.service.ts`: domain/business logic, usually in `shared/modules/*`
- `*.view.ts`: Discord message/view construction
- `*.interaction.ts`: component interaction handlers
- `*.types.ts`: local types and custom ID helpers
- `*.handler.ts`: bot-side orchestration around services/views
- `*.test.ts`: colocated tests
## Naming Conventions
## Runtime config
| Element | Convention | Example |
| ---------------- | ----------------------- | ---------------------------------------- |
| Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` |
| Classes | PascalCase | `CommandHandler`, `UserError` |
| Functions | camelCase | `createCommand`, `handleShopInteraction` |
| Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` |
| Enums | PascalCase | `TimerType`, `TransactionType` |
| Services | camelCase singleton | `economyService`, `userService` |
| Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` |
| DB tables | snake_case | `users`, `moderation_cases` |
| Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` |
- Global game settings live in `game_settings` and are loaded into `shared/lib/config.ts`.
- Guild-specific settings live in `guild_settings`; `getGuildConfig()` adds a 60-second cache on top of DB reads.
- Most numeric DB values exposed through runtime config are converted to `bigint` in `shared/lib/config.ts`.
## Code Patterns
## Interaction routing
### Command Definition
Global component routing is defined in `bot/lib/interaction.routes.ts` and consumed by `ComponentInteractionHandler`.
```typescript
export const commandName = createCommand({
data: new SlashCommandBuilder()
.setName("commandname")
.setDescription("Description"),
execute: async (interaction) => {
await interaction.deferReply();
// Implementation
},
});
```
Current route table:
### Service Pattern (Singleton Object)
- `trade_` and `amount` -> `bot/modules/trade/trade.interaction.ts`
- `shop_buy_` -> `bot/modules/economy/shop.interaction.ts`
- `lootdrop_` -> `bot/modules/economy/lootdrop.interaction.ts`
- `trivia_` -> `bot/modules/trivia/trivia.interaction.ts`
- `createitem_` -> `bot/modules/admin/item_wizard.ts`
- `enrollment` -> `bot/modules/user/enrollment.interaction.ts`
- `feedback_` -> `bot/modules/feedback/feedback.interaction.ts`
```typescript
export const serviceName = {
methodName: async (params: ParamType): Promise<ReturnType> => {
return await withTransaction(async (tx) => {
// Database operations
});
},
};
```
Some features still use local collectors instead of the global route table, notably inventory.
### Module File Organization
## Commands and access control
- `*.view.ts` - Creates Discord embeds/components
- `*.interaction.ts` - Handles button/select/modal interactions
- `*.types.ts` - Module-specific TypeScript types
- `*.service.ts` - Business logic (in shared/modules/)
- `*.test.ts` - Test files (co-located with source)
- Slash command execution is centralized in `bot/lib/handlers/CommandHandler.ts`.
- `withCommandErrorHandling()` is the normal command wrapper for defer/reply/error behavior.
- Beta commands rely on `featureFlagsService.hasAccess()`.
- `ADMIN_USER_IDS` controls admin panel access, not Discord permissions inside command code.
## Error Handling
## API and panel
### Custom Error Classes
- API routes are prefix-matched in `api/src/routes/index.ts`.
- `/auth/*` and `/api/health` are public.
- Players may access `/api/stats`, `/api/health`, `/api/me`, and `/api/me/inventory`.
- Remaining `/api/*` routes are admin-only.
- The panel dev server proxies back to the Bun server; the integrated server serves `panel/dist` when built.
```typescript
import { UserError, SystemError } from "@shared/lib/errors";
## Database notes
// User-facing errors (shown to user)
throw new UserError("You don't have enough coins!");
// System errors (logged, generic message shown)
throw new SystemError("Database connection failed");
```
### Recommended: `withCommandErrorHandling`
Use the `withCommandErrorHandling` utility from `@lib/commandUtils` to standardize
error handling across all commands. It handles `deferReply`, `UserError` display,
and unexpected error logging automatically.
```typescript
import { withCommandErrorHandling } from "@lib/commandUtils";
export const myCommand = createCommand({
data: new SlashCommandBuilder()
.setName("mycommand")
.setDescription("Does something"),
execute: async (interaction) => {
await withCommandErrorHandling(
interaction,
async () => {
const result = await service.method();
await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
},
{ ephemeral: true } // optional: makes the deferred reply ephemeral
);
},
});
```
Options:
- `ephemeral` — whether `deferReply` should be ephemeral
- `successMessage` — a simple string to send on success
- `onSuccess` — a callback invoked with the operation result
```
## Database Patterns
### Transaction Usage
```typescript
import { withTransaction } from "@/lib/db";
return await withTransaction(async (tx) => {
const user = await tx.query.users.findFirst({
where: eq(users.id, discordId),
});
await tx
.update(users)
.set({ coins: newBalance })
.where(eq(users.id, discordId));
await tx.insert(transactions).values({ userId: discordId, amount, type });
return user;
}, existingTx); // Pass existing tx if in nested transaction
```
### Schema Notes
- Use `bigint` mode for Discord IDs and currency amounts
- Relations defined separately from table definitions
- Schema modules: `shared/db/schema/*.ts` (users, inventory, economy, quests, moderation)
- Docker Compose uses PostgreSQL 17.
- Discord IDs and currency/xp values are stored as `bigint`.
- `withTransaction()` lives in `bot/lib/db.ts` and is the normal way shared services compose DB work.
## Testing
### Test File Structure
- Tests use `bun:test`.
- Mock modules before importing the unit under test.
- Most service tests stub `DrizzleClient` or `withTransaction()` rather than hitting the real database.
```typescript
import { describe, it, expect, mock, beforeEach } from "bun:test";
## Key entrypoints
// Mock modules BEFORE imports
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: { query: mockQuery },
}));
describe("serviceName", () => {
beforeEach(() => {
mockFn.mockClear();
});
it("should handle expected case", async () => {
// Arrange
mockFn.mockResolvedValue(testData);
// Act
const result = await service.method(input);
// Assert
expect(result).toEqual(expected);
expect(mockFn).toHaveBeenCalledWith(expectedArgs);
});
});
```
## Tech Stack
- **Runtime:** Bun 1.0+
- **Bot:** Discord.js 14.x
- **Web:** Bun HTTP Server (REST API)
- **Database:** PostgreSQL 16+ with Drizzle ORM
- **UI:** Discord embeds and components
- **Validation:** Zod
- **Testing:** Bun Test
- **Container:** Docker
## Key Files Reference
| Purpose | File |
| ------------- | ---------------------- |
| Bot entry | `bot/index.ts` |
| DB schema | `shared/db/schema.ts` |
| Error classes | `shared/lib/errors.ts` |
| Config loader | `shared/lib/config.ts` |
| Environment | `shared/lib/env.ts` |
| Embed helpers | `bot/lib/embeds.ts` |
| Command utils | `shared/lib/utils.ts` |
| Error handler | `bot/lib/commandUtils.ts` |
- `bot/index.ts`
- `bot/lib/BotClient.ts`
- `api/src/server.ts`
- `api/src/routes/index.ts`
- `shared/lib/config.ts`
- `shared/db/DrizzleClient.ts`
- `shared/db/schema/index.ts`

187
CLAUDE.md
View File

@@ -1,187 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Development
bun --watch bot/index.ts # Run bot + API with hot reload
docker compose up # Start all services (bot, API, database)
docker compose up app # Start just the app (bot + API)
docker compose up db # Start just the database
# Testing
bun test # Run all tests
bun test path/to/file.test.ts # Run a single test file
bun test shared/modules/economy # Run tests in a directory
bun test --watch # Watch mode
# Database
bun run db:push:local # Push schema changes (local)
bun run db:studio # Open Drizzle Studio (localhost:4983)
bun run generate # Generate Drizzle migrations (Docker)
bun run migrate # Apply migrations (Docker)
# Admin Panel
bun run panel:dev # Start Vite dev server for dashboard
bun run panel:build # Build React dashboard for production
```
## Architecture
Aurora is a Discord RPG bot + REST API running as a **single Bun process**. The bot and API share the same database client and services.
```
bot/ # Discord bot
├── commands/ # Slash commands by category (admin, economy, inventory, etc.)
├── events/ # Discord event handlers
├── lib/ # BotClient, handlers, loaders, embed helpers, commandUtils
├── modules/ # Feature modules (views, interactions per domain)
└── graphics/ # Canvas-based image generation (@napi-rs/canvas)
shared/ # Shared between bot and API
├── db/ # Drizzle ORM client + schema (users, economy, inventory, quests, etc.)
├── lib/ # env, config, errors, logger, types, utils
└── modules/ # Domain services (economy, user, inventory, quest, moderation, etc.)
api/ # REST API (Bun HTTP server)
└── src/routes/ # Route handlers for each domain
panel/ # React admin dashboard (Vite + Tailwind + Radix UI)
```
**Key architectural details:**
- Bot and API both import from `shared/` — do not duplicate logic.
- Services in `shared/modules/` are singleton objects, not classes.
- The database uses PostgreSQL 16+ via Drizzle ORM with `bigint` mode for Discord IDs and currency.
- Feature modules follow a strict file suffix convention (see below).
## Import Conventions
Use path aliases (defined in `tsconfig.json`). Order: external packages → aliases → relative.
```typescript
import { SlashCommandBuilder } from "discord.js"; // external
import { economyService } from "@shared/modules/economy/economy.service"; // alias
import { users } from "@db/schema"; // alias
import { createErrorEmbed } from "@lib/embeds"; // alias
import { localHelper } from "./helper"; // relative
```
**Aliases:**
- `@/*``bot/`
- `@shared/*``shared/`
- `@db/*``shared/db/`
- `@lib/*``bot/lib/`
- `@modules/*``bot/modules/`
- `@commands/*``bot/commands/`
## Code Patterns
### Module File Suffixes
- `*.view.ts` — Creates Discord embeds/components
- `*.interaction.ts` — Handles button/select/modal interactions
- `*.service.ts` — Business logic (lives in `shared/modules/`)
- `*.types.ts` — Module-specific TypeScript types
- `*.test.ts` — Tests (co-located with source)
### Command Definition
```typescript
export const commandName = createCommand({
data: new SlashCommandBuilder().setName("name").setDescription("desc"),
execute: async (interaction) => {
await withCommandErrorHandling(interaction, async () => {
const result = await service.method();
await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
}, { ephemeral: true });
},
});
```
`withCommandErrorHandling` (from `@lib/commandUtils`) handles `deferReply`, `UserError` display, and unexpected error logging automatically.
### Service Pattern
```typescript
export const serviceName = {
methodName: async (params: ParamType): Promise<ReturnType> => {
return await withTransaction(async (tx) => {
// database operations
});
},
};
```
### Error Handling
```typescript
import { UserError, SystemError } from "@shared/lib/errors";
throw new UserError("You don't have enough coins!"); // shown to user
throw new SystemError("DB connection failed"); // logged, generic message shown
```
### Database Transactions
```typescript
import { withTransaction } from "@/lib/db";
return await withTransaction(async (tx) => {
const user = await tx.query.users.findFirst({ where: eq(users.id, id) });
await tx.update(users).set({ coins: newBalance }).where(eq(users.id, id));
return user;
}, existingTx); // pass existing tx for nested transactions
```
### Testing
Mock modules **before** imports. Use `bun:test`.
```typescript
import { describe, it, expect, mock, beforeEach } from "bun:test";
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: { query: mockQuery },
}));
describe("serviceName", () => {
beforeEach(() => mockFn.mockClear());
it("should handle expected case", async () => {
mockFn.mockResolvedValue(testData);
const result = await service.method(input);
expect(result).toEqual(expected);
});
});
```
## Naming Conventions
| Element | Convention | Example |
| ---------------- | ---------------------- | -------------------------------- |
| Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` |
| Classes | PascalCase | `CommandHandler`, `UserError` |
| Functions | camelCase | `createCommand`, `handleShopInteraction` |
| Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` |
| Enums | PascalCase | `TimerType`, `TransactionType` |
| Services | camelCase singleton | `economyService`, `userService` |
| Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` |
| DB tables | snake_case | `users`, `moderation_cases` |
| Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` |
| API routes | kebab-case | `/api/guild-settings` |
## Key Files
| Purpose | File |
| ----------------- | -------------------------- |
| Bot entry point | `bot/index.ts` |
| Discord client | `bot/lib/BotClient.ts` |
| DB schema index | `shared/db/schema.ts` |
| Error classes | `shared/lib/errors.ts` |
| Environment vars | `shared/lib/env.ts` |
| Config loader | `shared/lib/config.ts` |
| Embed helpers | `bot/lib/embeds.ts` |
| Command utils | `bot/lib/commandUtils.ts` |
| API server | `api/src/server.ts` |

270
README.md
View File

@@ -1,159 +1,181 @@
# Aurora
> A comprehensive, feature-rich Discord RPG bot built with modern technologies.
Aurora is a Discord RPG bot, admin/player panel, and REST/WebSocket API that run as one Bun application. The Discord bot and HTTP server share the same database client, config, services, and domain events.
![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![Bun](https://img.shields.io/badge/Bun-1.0+-black)
![Discord.js](https://img.shields.io/badge/Discord.js-14.x-5865F2)
![Drizzle ORM](https://img.shields.io/badge/Drizzle_ORM-0.30+-C5F74F)
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791)
## What exists today
Aurora is a powerful Discord bot designed to facilitate RPG-like elements within a Discord server. It features a robust economy, class system, inventory management, quests, and more, all built on top of a high-performance stack using Bun and Drizzle ORM.
- Discord slash commands for economy, inventory, quests, moderation, feedback, user profiles, and admin tooling.
- A Bun HTTP API under `/api/*`, Discord OAuth under `/auth/*`, and a WebSocket endpoint at `/ws`.
- A React panel for both admins and enrolled players.
- Shared domain services in `shared/modules/*` and reusable game plugins in `shared/games/*`.
- Built-in real-time games: chess and blackjack.
**New in v1.0:** Aurora now includes a fully integrated **REST API** for accessing bot data, statistics, and configuration, running alongside the bot in a single process.
## Architecture
## ✨ Features
```text
bot/ Discord bot entrypoint, commands, events, Discord-facing views/interactions
api/ Bun HTTP server, route modules, WebSocket/game room server
panel/ React 19 + Vite + Tailwind v4 dashboard
shared/ Shared DB schema, services, config, events, utilities, game plugins
docs/ Product and design notes
```
### Discord Bot
* **Class System**: Users can join different classes.
* **Economy**: Complete economy system with balance, transactions, and daily rewards.
* **Inventory & Items**: Sophisticated item system with rarities, types (Material, Consumable, etc.), and inventory management.
* **Leveling**: XP-based leveling system to track user activity and progress.
* **Quests**: Quest system with requirements and rewards.
* **Trading**: Secure trading system between users.
* **Lootdrops**: Random loot drops in channels to engage users.
* **Admin Tools**: Administrative commands for server management.
Important points:
### REST API
* **Live Analytics**: Real-time statistics endpoint (commands, transactions).
* **Configuration Management**: Update bot settings via API.
* **Database Inspection**: Integrated Drizzle Studio access.
* **WebSocket Support**: Real-time event streaming for live updates.
- `bot/index.ts` initializes DB-backed config, wires domain events, starts the API server, then logs into Discord.
- The API server also serves built panel assets from `panel/dist` when they exist.
- Bot commands, API routes, and the panel all rely on the same service layer in `shared/modules/*`.
- Runtime game config is loaded from the `game_settings` table into `shared/lib/config.ts`.
## 🏗️ Architecture
Aurora uses a **Single Process Monolith** architecture to maximize performance and simplify resource sharing.
* **Unified Runtime**: Both the Discord Client and the REST API run within the same Bun process.
* **Shared State**: This allows the API to access live bot memory (caches, gateways) directly without complex inter-process communication (IPC).
* **Simplified Deployment**: You only need to deploy a single Docker container.
## 🛠️ Tech Stack
* **Runtime**: [Bun](https://bun.sh/)
* **Bot Framework**: [Discord.js](https://discord.js.org/)
* **API Framework**: Bun HTTP Server (REST API)
* **UI**: Discord embeds and components
* **Database**: [PostgreSQL](https://www.postgresql.org/)
* **ORM**: [Drizzle ORM](https://orm.drizzle.team/)
* **Validation**: [Zod](https://zod.dev/)
* **Containerization**: [Docker](https://www.docker.com/)
## 🚀 Getting Started
## Getting started
### Prerequisites
* [Bun](https://bun.sh/) (latest version)
* [Docker](https://www.docker.com/) & Docker Compose
- Bun
- Docker and Docker Compose
- A Discord application with bot token, client ID, and client secret
### Installation
### Setup
1. **Clone the repository**
```bash
git clone <repository-url>
cd aurora
```
1. Install dependencies.
2. **Install dependencies**
```bash
bun install
```
```bash
bun install
```
3. **Environment Setup**
Copy the example environment file and configure it:
```bash
cp .env.example .env
```
Edit `.env` with your Discord bot token, Client ID, and database credentials.
2. Create your environment file.
> **Note**: The `DATABASE_URL` in `.env.example` is pre-configured for Docker.
```bash
cp .env.example .env
```
4. **Start the Database**
Run the database service using Docker Compose:
```bash
docker compose up -d db
```
3. Start PostgreSQL.
5. **Run Migrations**
```bash
bun run migrate
```
OR
```bash
bun run db:push
```
```bash
docker compose up -d db
```
### Running the Bot & API
4. Initialize the schema.
```bash
bun run db:push:local
```
If you prefer running schema changes through Docker:
```bash
bun run migrate
```
5. Start the bot and API.
**Development Mode** (with hot reload):
```bash
bun run dev
```
* Bot: Online in Discord
* API: http://localhost:3000
**Production Mode**:
Build and run with Docker (recommended):
The Bun server listens on `http://localhost:3000`.
### Panel development
The Bun server can serve a built panel, but day-to-day panel work is done with Vite:
```bash
docker compose up -d app
bun run panel:dev
```
### 🔐 Accessing Production Services (SSH Tunnel)
The panel dev server runs on `http://localhost:5173` and proxies `/api`, `/auth`, `/assets`, and `/ws` to `http://localhost:3000`.
For security, the Production Database and API are **not exposed** to the public internet by default. They are only accessible via localhost on the server.
To build the panel for the integrated Bun server:
To access them from your local machine, use the included SSH tunnel script.
1. Add your VPS details to your local `.env` file:
```env
VPS_USER=root
VPS_HOST=123.45.67.89
```
2. Run the remote connection script:
```bash
bun run remote
```
This will establish secure tunnels for:
* **API**: http://localhost:3000
* **Drizzle Studio**: http://localhost:4983
## 📜 Scripts
* `bun run dev`: Start the bot and API server in watch mode.
* `bun run remote`: Open SSH tunnel to production services.
* `bun run generate`: Generate Drizzle migrations.
* `bun run migrate`: Apply migrations (via Docker).
* `bun run db:studio`: Open Drizzle Studio to inspect the database.
* `bun test`: Run tests.
## 📂 Project Structure
```
├── bot # Discord Bot logic & entry point
├── web # REST API Server
├── shared # Shared code (Database, Config, Types)
├── drizzle # Drizzle migration files
├── scripts # Utility scripts
├── docker-compose.yml
└── package.json
```bash
bun run panel:build
```
## 🤝 Contributing
## Useful scripts
Contributions are welcome! Please feel free to submit a Pull Request.
```bash
# App
bun run dev
docker compose up
docker compose up app
docker compose up db
## 📄 License
# Database
bun run db:push
bun run db:push:local
bun run db:generate
bun run db:migrate
bun run db:studio
bun run db:backup
bun run db:restore
This project is licensed under the MIT License.
# Panel
bun run panel:dev
bun run panel:build
# Tests
bun test
bun run test
bun run test:ci
# Ops
bun run remote
bun run deploy
bun run deploy:remote
```
## Environment notes
The main variables you need in `.env` are:
- `DISCORD_BOT_TOKEN`
- `DISCORD_CLIENT_ID`
- `DISCORD_CLIENT_SECRET`
- `DISCORD_GUILD_ID`
- `ADMIN_USER_IDS`
- `SESSION_SECRET`
- `DB_USER`
- `DB_PASSWORD`
- `DB_NAME`
- `DATABASE_URL`
- `PANEL_BASE_URL`
Players can authenticate into the panel only after they exist in the `users` table. Admin access is determined by `ADMIN_USER_IDS`, and panel sessions are stored in signed cookies keyed by `SESSION_SECRET`.
## API and panel summary
- Public routes: `/auth/*`, `/api/health`
- Player-accessible API routes: `/api/stats`, `/api/health`, `/api/me`, `/api/me/inventory`
- Admin-only API routes: the rest of `/api/*`
- WebSocket: `/ws` with cookie-based auth
- Static assets: `/assets/*`
## Project structure
```text
bot/
commands/
events/
lib/
modules/
api/
src/
routes/
games/
panel/
src/
shared/
db/
games/
lib/
modules/
```
## Documentation
- [AGENTS.md](AGENTS.md): repo-wide implementation guidance
- [api/README.md](api/README.md): API surface and auth model
- [docs/new-design/DESIGN.md](docs/new-design/DESIGN.md): current panel design language

View File

@@ -1,30 +1,130 @@
# Aurora Web API
# Aurora API
The web API provides a REST interface and WebSocket support for accessing Aurora bot data and configuration.
Aurora's API is a Bun server that runs inside the same process as the Discord bot. It serves REST routes, the authenticated WebSocket endpoint, static assets, and built panel files.
## API Endpoints
## Runtime model
- `GET /api/stats` - Real-time bot statistics
- `GET /api/settings` - Bot configuration
- `GET /api/users` - User data
- `GET /api/items` - Item catalog
- `GET /api/quests` - Quest information
- `GET /api/transactions` - Economy data
- `GET /api/health` - Health check
- Entry point: `api/src/server.ts`
- Route dispatcher: `api/src/routes/index.ts`
- Auth: Discord OAuth with signed session cookies
- WebSocket: `/ws`
- Static assets: `/assets/*`
- Built panel fallback: `panel/dist`
## Access model
Public:
- `GET /api/health`
- `/auth/discord`
- `/auth/callback`
- `POST /auth/logout`
- `GET /auth/me`
Player-accessible API routes:
- `GET /api/stats`
- `GET /api/health`
- `GET /api/me`
- `GET /api/me/inventory`
Admin-only API routes:
- everything else under `/api/*`
Admin vs player is derived from `ADMIN_USER_IDS`. A user must already exist in the `users` table to complete panel login.
## Route summary
### Auth
- `GET /auth/discord`
- `GET /auth/callback`
- `POST /auth/logout`
- `GET /auth/me`
### Dashboard and system
- `GET /api/health`
- `GET /api/stats`
- `GET /api/stats/activity`
- `POST /api/actions/reload-commands`
- `POST /api/actions/clear-cache`
- `POST /api/actions/maintenance-mode`
### Settings
- `GET /api/settings`
- `POST /api/settings`
- `GET /api/settings/meta`
- `GET /api/guilds/:guildId/settings`
- `PUT|PATCH /api/guilds/:guildId/settings`
- `DELETE /api/guilds/:guildId/settings`
### Users, classes, and inventory
- `GET /api/me`
- `GET /api/me/inventory`
- `GET /api/users`
- `GET /api/users/:id`
- `PUT /api/users/:id`
- `GET /api/users/:id/inventory`
- `POST /api/users/:id/inventory`
- `DELETE /api/users/:id/inventory/:itemId`
- `GET /api/classes`
- `POST /api/classes`
- `PUT /api/classes/:id`
- `DELETE /api/classes/:id`
### Game content
- `GET /api/items`
- `POST /api/items`
- `GET /api/items/:id`
- `PUT /api/items/:id`
- `DELETE /api/items/:id`
- `POST /api/items/:id/icon`
- `GET /api/quests`
- `POST /api/quests`
- `PUT /api/quests/:id`
- `DELETE /api/quests/:id`
- `GET /api/lootdrops`
- `POST /api/lootdrops`
- `DELETE /api/lootdrops/:messageId`
### Moderation and economy history
- `GET /api/moderation`
- `POST /api/moderation`
- `GET /api/transactions`
## WebSocket
Connect to `/ws` for real-time updates:
- Stats broadcasts every 5 seconds
- Event notifications via system bus
- PING/PONG heartbeat support
`/ws` requires a valid `aurora_session` cookie.
Current behavior:
- dashboard clients subscribe to `dashboard`
- game clients also use lobby and room-scoped traffic through `GameServer`
- `PING` from the client returns `PONG`
- dashboard stats are broadcast every 5 seconds while at least one client is connected
- hard limits in `api/src/server.ts`:
- 200 concurrent connections
- 16 KB max payload
- 60 second idle timeout
## Development
The API runs automatically when you start the bot:
Start the backend:
```bash
bun run dev
```
The API will be available at `http://localhost:3000`
Optional panel dev server:
```bash
bun run panel:dev
```
Panel dev runs on `http://localhost:5173` and proxies API/auth/assets/WebSocket requests to `http://localhost:3000`.

68
api/src/AGENTS.md Normal file
View File

@@ -0,0 +1,68 @@
# API layer
## Server shape
- Aurora uses Bun's native `serve()` API in `api/src/server.ts`.
- Route modules are aggregated in `api/src/routes/index.ts`.
- A route module returns `null` when it does not match so the dispatcher can continue.
- After route handling, the server tries `panel/dist` for SPA/static files.
## Authentication and authorization
- OAuth routes live in `api/src/routes/auth.routes.ts`.
- Sessions are stored in signed `aurora_session` cookies.
- Session TTL is 7 days.
- Login succeeds only for users already present in the `users` table.
- Role is `admin` if the Discord ID is in `ADMIN_USER_IDS`, otherwise `player`.
- Redirects after login are intentionally restricted to localhost or relative paths.
Current access rules from `api/src/routes/index.ts`:
- public: `/auth/*`, `/api/health`
- player allow-list: `/api/stats`, `/api/health`, `/api/me`
- everything else under `/api/*`: admin-only
`/api/me/inventory` is handled by `users.routes.ts` and still depends on a valid session.
## Response conventions
- `jsonResponse()` serializes `bigint` values as strings.
- `errorResponse()` returns `{ error, details? }`.
- `parseBody()` and `parseQuery()` validate with Zod and return a `Response` on failure.
- The API does not use a framework-level middleware stack; each route handles its own parsing and branching.
## WebSocket
- Endpoint: `/ws`
- Requires an authenticated session
- Dashboard channel: `dashboard`
- Lobby channel: `lobby`
- Room-specific messaging is handled inside `GameServer`
- Dashboard broadcasts `STATS_UPDATE` every 5 seconds while clients are connected
- `NEW_EVENT` broadcasts are wired from `shared/lib/events`
Hard limits:
- max connections: 200
- max payload: 16 KB
- idle timeout: 60 seconds
## Static files
- Built panel assets are served from `panel/dist`
- `/assets/*` serves files from `bot/assets/graphics`
- `/api/*`, `/auth/*`, `/ws`, and `/assets/*` bypass the SPA fallback
## Route notes
- `items.routes.ts` supports both JSON and multipart form data for item creation.
- `settings.routes.ts` writes DB-backed game settings and emits the reload-commands event.
- `guild-settings.routes.ts` invalidates the guild config cache after writes.
- `lootdrops.routes.ts` delegates spawning/deletion to bot-side handlers because Discord message creation happens there.
## Gotchas
- Some runtime caches are in-memory only and are lost on restart.
- The server registers game plugins at startup; duplicate registration throws.
- BigInt-safe JSON matters for nearly every domain route.
- The panel's auth flow depends on `PANEL_BASE_URL` matching the OAuth callback origin.

540
api/src/games/GameServer.ts Normal file
View File

@@ -0,0 +1,540 @@
import { RoomManager } from "./RoomManager";
import { GameWsClientSchema } from "./types";
import type { PlayerInfo } from "./types";
import { logger } from "@shared/lib/logger";
import { economyService } from "@shared/modules/economy/economy.service";
import { TransactionType } from "@shared/lib/constants";
import { gameRegistry } from "@shared/games/registry";
import type { Server, ServerWebSocket } from "bun";
export interface WsConnectionData {
session: { discordId: string; username: string; role: string };
rooms: Set<string>;
}
export class GameServer {
readonly roomManager = new RoomManager();
private connections = new Map<string, ServerWebSocket<WsConnectionData>>();
private replacedConnections = new Map<string, ServerWebSocket<WsConnectionData>>();
private bunServer: Server<WsConnectionData> | null = null;
constructor() {
// Subscribe to room events and route them to the right clients
this.roomManager.emitter.on("room:created", ({ roomId, gameSlug }) => {
// The creating connection will subscribe itself; just broadcast room list
this.publishRoomListUpdate();
});
this.roomManager.emitter.on("game:started", ({ roomId, spectatorView, playerViews }) => {
// Send personalised state to each player
for (const [playerId, view] of playerViews) {
this.sendToPlayer(playerId, {
type: "GAME_STATE",
roomId,
state: view,
});
}
// Broadcast started event with spectator view to the room channel
this.publish(`room:${roomId}`, {
type: "GAME_STARTED",
roomId,
state: spectatorView,
});
});
this.roomManager.emitter.on("game:updated", ({ roomId, spectatorView, playerViews }) => {
// Each player gets their personalised view directly
for (const [playerId, view] of playerViews) {
this.sendToPlayer(playerId, {
type: "GAME_STATE",
roomId,
state: view,
});
}
// Spectators/others get the spectator view via pub/sub
this.publish(`room:${roomId}`, {
type: "GAME_UPDATE",
roomId,
state: spectatorView,
});
});
this.roomManager.emitter.on("game:ended", ({ roomId, winner, reason, payouts }) => {
const room = this.roomManager.getRoom(roomId);
const betAmount = room?.betAmount ?? 0;
// Handle bet payouts asynchronously — broadcast happens after settlement
if (betAmount > 0) {
this.settleBets(roomId, winner, betAmount, payouts).then((payout) => {
this.publish(`room:${roomId}`, {
type: "GAME_ENDED",
roomId,
winner,
reason,
payout,
});
this.publishRoomListUpdate();
});
return;
}
this.publish(`room:${roomId}`, {
type: "GAME_ENDED",
roomId,
winner,
reason,
});
this.publishRoomListUpdate();
});
this.roomManager.emitter.on("round:settled", async ({ roomId, roundSettlements }) => {
const room = this.roomManager.getRoom(roomId);
if (!room || room.betAmount <= 0) return;
const gameName = gameRegistry.get(room.gameSlug)?.name ?? "Game";
const settlementDetails: typeof roundSettlements = {};
for (const [playerId, settlement] of Object.entries(roundSettlements)) {
try {
if (settlement.payout > 0) {
await economyService.modifyUserBalance(
playerId,
BigInt(settlement.payout),
TransactionType.GAME_WIN,
`${gameName} round payout (room ${roomId.slice(0, 8)})`,
);
}
settlementDetails[playerId] = settlement;
} catch (err) {
logger.error("web", `Round payout failed for ${playerId} in room ${roomId}: ${err}`);
}
}
if (Object.keys(settlementDetails).length > 0) {
this.publish(`room:${roomId}`, { type: "ROUND_SETTLED", roomId, settlements: settlementDetails });
}
});
this.roomManager.emitter.on("player:left", ({ roomId, playerId }) => {
this.publish(`room:${roomId}`, {
type: "PLAYER_LEFT",
roomId,
playerId,
});
});
this.roomManager.emitter.on("room:deleted", ({ roomId }) => {
const channel = `room:${roomId}`;
for (const [, ws] of this.connections) {
if (ws.data.rooms.has(roomId)) {
ws.send(JSON.stringify({ type: "ERROR", message: "Room not found" }));
ws.unsubscribe(channel);
ws.data.rooms.delete(roomId);
}
}
});
this.roomManager.emitter.on("room:list:changed", () => {
this.publishRoomListUpdate();
});
}
setServer(server: Server<WsConnectionData>): void {
this.bunServer = server;
}
handleOpen(ws: ServerWebSocket<WsConnectionData>): void {
const discordId = ws.data.session.discordId;
const existing = this.connections.get(discordId);
if (existing && existing !== ws) {
this.replacedConnections.set(discordId, existing);
}
this.connections.set(discordId, ws);
ws.send(JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: this.roomManager.listRooms() }));
}
async handleMessage(ws: ServerWebSocket<WsConnectionData>, raw: unknown): Promise<void> {
const parsed = GameWsClientSchema.safeParse(raw);
if (!parsed.success) {
ws.send(JSON.stringify({ type: "ERROR", message: "Invalid message format" }));
return;
}
const msg = parsed.data;
const { discordId, username, role } = ws.data.session;
switch (msg.type) {
case "CREATE_ROOM": {
// Solo mode forces betAmount to 0
const options = msg.options ? { ...msg.options } : {};
if (options.soloMode) options.betAmount = 0;
const result = this.roomManager.createRoom(msg.gameType, discordId, options);
if (!result.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;
}
ws.subscribe(`room:${result.roomId}`);
ws.data.rooms.add(result.roomId);
ws.send(JSON.stringify({ type: "ROOM_CREATED", roomId: result.roomId, gameSlug: msg.gameType }));
logger.info("web", `Room created: ${result.roomId} (${msg.gameType}) by ${discordId}`);
// Solo mode: auto-fill and start immediately
if (options.soloMode) {
const fillResult = this.roomManager.fillRoom(result.roomId, discordId);
if (!fillResult.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: fillResult.error }));
}
// fillRoom with betAmount=0 calls startGame internally
}
// Auto-start if room is immediately full (e.g. maxPlayers: 1) — skip for manualStart games
const plugin = gameRegistry.get(msg.gameType);
const createdRoom = this.roomManager.getRoom(result.roomId);
if (!options.soloMode && plugin && !plugin.manualStart && createdRoom && createdRoom.players.length >= plugin.maxPlayers && createdRoom.status === "waiting") {
if (createdRoom.betAmount > 0) {
this.deductBetsAndStart(result.roomId, createdRoom.betAmount, createdRoom.players, ws);
} else {
this.roomManager.startGame(result.roomId);
}
}
break;
}
case "JOIN_ROOM": {
const result = this.roomManager.joinRoom(msg.roomId, discordId, msg.preferAs, msg.role ?? role);
if (!result.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;
}
ws.subscribe(`room:${msg.roomId}`);
ws.data.rooms.add(msg.roomId);
const room = this.roomManager.getRoom(msg.roomId);
const roomStatus = room?.status ?? "waiting";
// Determine the current state to send to this client
let state: unknown = undefined;
if (room && room.status === "playing") {
state = result.joinedAs === "spectator"
? this.roomManager.getSpectatorView(msg.roomId)
: this.roomManager.getPlayerView(msg.roomId, discordId);
}
// Build player/spectator lists for JOIN_RESULT
const resolveInfo = (ids: string[]): PlayerInfo[] =>
ids.map(id => ({
discordId: id,
username: this.connections.get(id)?.data.session.username ?? id,
}));
const players = resolveInfo(room?.players ?? []);
const spectators = resolveInfo(Array.from(room?.spectators ?? new Set()));
// Notify replaced connection in the same room (multi-tab detection)
const replacedWs = this.replacedConnections.get(discordId);
if (replacedWs && replacedWs.data.rooms.has(msg.roomId)) {
replacedWs.send(JSON.stringify({ type: "SESSION_REPLACED", roomId: msg.roomId }));
replacedWs.data.rooms.delete(msg.roomId);
replacedWs.unsubscribe(`room:${msg.roomId}`);
}
this.replacedConnections.delete(discordId);
// Build room options for the client
const roomOptions = room
? {
...(room.betAmount > 0 ? { betAmount: room.betAmount } : {}),
...(typeof room.options?.timeControl === "string" ? { timeControl: room.options.timeControl } : {}),
}
: undefined;
// Respond with JOIN_RESULT
ws.send(JSON.stringify({
type: "JOIN_RESULT",
roomId: msg.roomId,
joinedAs: result.joinedAs,
roomStatus,
players,
spectators,
state,
roomOptions,
}));
// Notify other room members
const playerInfo: PlayerInfo = { discordId, username };
this.publish(`room:${msg.roomId}`, {
type: "PLAYER_JOINED",
roomId: msg.roomId,
player: playerInfo,
joinedAs: result.joinedAs,
});
logger.info("web", `${discordId} joined room ${msg.roomId} as ${result.joinedAs}`);
// Handle async bet deduction when room is ready to start
if (result.readyToStart && room) {
this.deductBetsAndStart(msg.roomId, room.betAmount, room.players, ws);
}
break;
}
case "LEAVE_ROOM": {
this.roomManager.leaveRoom(msg.roomId, discordId);
ws.unsubscribe(`room:${msg.roomId}`);
ws.data.rooms.delete(msg.roomId);
break;
}
case "GAME_ACTION": {
// Action cost pre-check: deduct bet before processing split/double/place_bet
const actionRoom = this.roomManager.getRoom(msg.roomId);
if (actionRoom && actionRoom.betAmount > 0 && actionRoom.state) {
const actionPlugin = gameRegistry.get(actionRoom.gameSlug);
if (actionPlugin?.getActionCost) {
const cost = actionPlugin.getActionCost(actionRoom.state, msg.action, discordId);
if (cost > 0) {
const amount = actionRoom.betAmount * cost;
const gameName = actionPlugin.name ?? actionRoom.gameSlug;
try {
await economyService.modifyUserBalance(
discordId,
-BigInt(amount),
TransactionType.GAME_BET,
`${gameName} action bet (room ${msg.roomId.slice(0, 8)})`,
);
} catch {
ws.send(JSON.stringify({ type: "ERROR", message: "Insufficient funds for this action" }));
return;
}
}
}
}
const result = this.roomManager.handleAction(msg.roomId, discordId, msg.action);
if (!result.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;
}
// game:updated event handler will dispatch views to players and spectators
break;
}
case "START_GAME": {
const room = this.roomManager.getRoom(msg.roomId);
if (!room) {
ws.send(JSON.stringify({ type: "ERROR", message: "Room not found" }));
return;
}
if (room.host !== discordId) {
ws.send(JSON.stringify({ type: "ERROR", message: "Only the host can start the game" }));
return;
}
if (room.status !== "waiting") {
ws.send(JSON.stringify({ type: "ERROR", message: "Game is not in waiting state" }));
return;
}
const startPlugin = gameRegistry.get(room.gameSlug);
if (startPlugin && room.players.length < startPlugin.minPlayers) {
ws.send(JSON.stringify({ type: "ERROR", message: `Need at least ${startPlugin.minPlayers} player(s) to start` }));
return;
}
if (room.betAmount > 0) {
this.deductBetsAndStart(msg.roomId, room.betAmount, room.players, ws);
} else {
const startResult = this.roomManager.startGame(msg.roomId);
if (!startResult.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: startResult.error }));
}
}
logger.info("web", `Host ${discordId} started game in room ${msg.roomId}`);
break;
}
case "FILL_ROOM": {
if (role !== "admin") {
ws.send(JSON.stringify({ type: "ERROR", message: "Only admins can fill a room for solo testing" }));
return;
}
const fillResult = this.roomManager.fillRoom(msg.roomId, discordId);
if (!fillResult.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: fillResult.error }));
return;
}
if (fillResult.readyToStart) {
const room = this.roomManager.getRoom(msg.roomId);
if (room) this.deductBetsAndStart(msg.roomId, room.betAmount, room.players, ws);
}
logger.info("web", `Admin ${discordId} filled room ${msg.roomId} for solo testing`);
break;
}
}
}
handleClose(ws: ServerWebSocket<WsConnectionData>): void {
// If this is a replaced (displaced) connection, just clean up the registry and stop
for (const [id, prevWs] of this.replacedConnections) {
if (prevWs === ws) {
this.replacedConnections.delete(id);
return;
}
}
for (const roomId of ws.data.rooms) {
this.roomManager.leaveRoom(roomId, ws.data.session.discordId);
}
this.connections.delete(ws.data.session.discordId);
}
/**
* Deduct bet amounts from all players, then start the game.
* If any player can't afford the bet, refund already-deducted players
* and remove the failing player from the room.
*/
private async deductBetsAndStart(
roomId: string,
betAmount: number,
playerIds: string[],
triggeringWs: ServerWebSocket<WsConnectionData>,
): Promise<void> {
const room = this.roomManager.getRoom(roomId);
if (!room || room.betsPending) return;
// Games with getActionCost handle per-round betting themselves (e.g., blackjack).
// Skip the upfront deduction — just start the game.
const plugin = gameRegistry.get(room.gameSlug);
if (plugin?.getActionCost) {
const startResult = this.roomManager.startGame(roomId);
if (!startResult.ok) {
triggeringWs.send(JSON.stringify({ type: "ERROR", message: startResult.error }));
}
return;
}
room.betsPending = true;
const uniquePlayers = [...new Set(playerIds)];
const deducted: string[] = [];
try {
const gameName = gameRegistry.get(room.gameSlug)?.name ?? room.gameSlug;
for (const pid of uniquePlayers) {
await economyService.modifyUserBalance(
pid,
-BigInt(betAmount),
TransactionType.GAME_BET,
`${gameName} wager (room ${roomId.slice(0, 8)})`,
);
deducted.push(pid);
}
// All deductions succeeded — start the game
const startResult = this.roomManager.startGame(roomId);
if (!startResult.ok) {
// Shouldn't happen, but refund if it does
await this.refundPlayers(deducted, betAmount, roomId);
}
} catch (err) {
// Refund anyone already deducted
await this.refundPlayers(deducted, betAmount, roomId);
// Find the player who couldn't afford the bet
const failedPlayer = uniquePlayers.find(p => !deducted.includes(p));
if (failedPlayer) {
this.roomManager.removePlayer(roomId, failedPlayer);
this.sendToPlayer(failedPlayer, {
type: "ERROR",
message: "Insufficient funds for the bet. You have been removed from the room.",
});
this.publish(`room:${roomId}`, {
type: "PLAYER_LEFT",
roomId,
playerId: failedPlayer,
});
}
logger.warn("web", `Bet deduction failed for room ${roomId}: ${err}`);
} finally {
if (room) room.betsPending = false;
}
}
/** Pay out winnings or refund bets on game end. */
private async settleBets(
roomId: string,
winner: string | null,
betAmount: number,
payouts?: Record<string, number>,
): Promise<{ amount: number; refunded?: boolean }> {
const room = this.roomManager.getRoom(roomId);
const uniquePlayers = [...new Set(room?.players ?? [])];
const gameName = gameRegistry.get(room?.gameSlug ?? "")?.name ?? "Game";
try {
// Custom payouts override default pot logic (used by house-edge games like blackjack)
if (payouts) {
let totalPaid = 0;
for (const [playerId, multiplier] of Object.entries(payouts)) {
if (multiplier <= 0) continue;
const amount = Math.floor(betAmount * multiplier);
await economyService.modifyUserBalance(
playerId,
BigInt(amount),
TransactionType.GAME_WIN,
`${gameName} payout (room ${roomId.slice(0, 8)})`,
);
totalPaid = Math.max(totalPaid, amount);
}
const isRefund = !winner && totalPaid === betAmount;
return { amount: totalPaid, refunded: isRefund };
}
// Default pot logic: winner takes all, draw refunds everyone
const pot = betAmount * uniquePlayers.length;
if (winner) {
await economyService.modifyUserBalance(
winner,
BigInt(pot),
TransactionType.GAME_WIN,
`${gameName} wager won (room ${roomId.slice(0, 8)})`,
);
return { amount: pot };
} else {
await this.refundPlayers(uniquePlayers, betAmount, roomId, gameName);
return { amount: betAmount, refunded: true };
}
} catch (err) {
logger.error("web", `Bet settlement failed for room ${roomId}: ${err}`);
return { amount: 0 };
}
}
private async refundPlayers(playerIds: string[], betAmount: number, roomId: string, gameName = "Game"): Promise<void> {
for (const pid of playerIds) {
try {
await economyService.modifyUserBalance(
pid,
BigInt(betAmount),
TransactionType.GAME_WIN,
`${gameName} wager refund (room ${roomId.slice(0, 8)})`,
);
} catch (err) {
logger.error("web", `Failed to refund ${pid} for room ${roomId}: ${err}`);
}
}
}
private publish(channel: string, message: unknown): void {
this.bunServer?.publish(channel, JSON.stringify(message));
}
private sendToPlayer(discordId: string, message: unknown): void {
this.connections.get(discordId)?.send(JSON.stringify(message));
}
private publishRoomListUpdate(): void {
this.publish("lobby", { type: "ROOM_LIST_UPDATE", rooms: this.roomManager.listRooms() });
}
}
export const gameServer = new GameServer();

View File

@@ -0,0 +1,187 @@
import { describe, it, expect, beforeEach } from "bun:test";
import { RoomManager } from "./RoomManager";
import { gameRegistry } from "@shared/games/registry";
import type { GamePlugin } from "@shared/games/types";
// Minimal stub plugin for testing the room system
const stubPlugin: GamePlugin<{ turn: number }, { type: string }> = {
slug: "stub",
name: "Stub Game",
minPlayers: 2,
maxPlayers: 2,
createInitialState: (players) => ({ turn: 0 }),
handleAction: (state, action, playerId) => ({ ok: true, state: { ...state, turn: state.turn + 1 } }),
getPlayerView: (state) => state,
getSpectatorView: (state) => state,
};
if (!gameRegistry.get("stub")) {
gameRegistry.register(stubPlugin);
}
describe("RoomManager", () => {
let manager: RoomManager;
beforeEach(() => {
manager = new RoomManager();
});
describe("createRoom", () => {
it("should create a room and return its id", () => {
const result = manager.createRoom("stub", "player1");
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.roomId).toBeDefined();
expect(typeof result.roomId).toBe("string");
}
});
it("should reject unknown game type", () => {
const result = manager.createRoom("unknown-game", "player1");
expect(result.ok).toBe(false);
});
it("should add creator as first player", () => {
const result = manager.createRoom("stub", "player1");
if (result.ok) {
const room = manager.getRoom(result.roomId);
expect(room?.players).toContain("player1");
expect(room?.host).toBe("player1");
expect(room?.status).toBe("waiting");
}
});
});
describe("joinRoom", () => {
it("should add a player to a waiting room", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
const join = manager.joinRoom(create.roomId, "player2", "player");
expect(join.ok).toBe(true);
if (join.ok) {
expect(join.joinedAs).toBe("player");
}
});
it("should auto-start when room reaches maxPlayers", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.joinRoom(create.roomId, "player2", "player");
const room = manager.getRoom(create.roomId);
expect(room?.status).toBe("playing");
expect(room?.state).toBeDefined();
});
it("should allow joining as spectator when game is playing", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.joinRoom(create.roomId, "player2", "player");
const spec = manager.joinRoom(create.roomId, "spectator1", "spectator");
expect(spec.ok).toBe(true);
});
it("should downgrade to spectator when joining full room as player", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.joinRoom(create.roomId, "player2", "player");
const result = manager.joinRoom(create.roomId, "player3", "player");
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.joinedAs).toBe("spectator");
}
});
it("should reject joining nonexistent room", () => {
const result = manager.joinRoom("fake-id", "player1", "player");
expect(result.ok).toBe(false);
});
});
describe("handleAction", () => {
it("should apply a valid game action", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.joinRoom(create.roomId, "player2", "player");
const result = manager.handleAction(create.roomId, "player1", { type: "action" });
expect(result.ok).toBe(true);
});
it("should reject action from spectator", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.joinRoom(create.roomId, "player2", "player");
manager.joinRoom(create.roomId, "spectator1", "spectator");
const result = manager.handleAction(create.roomId, "spectator1", { type: "action" });
expect(result.ok).toBe(false);
});
});
describe("leaveRoom", () => {
it("should remove a player from the room", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.leaveRoom(create.roomId, "player1");
const room = manager.getRoom(create.roomId);
expect(room).toBeUndefined();
});
it("should remove a spectator from the room", () => {
const create = manager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
manager.joinRoom(create.roomId, "player2", "player");
manager.joinRoom(create.roomId, "spec1", "spectator");
manager.leaveRoom(create.roomId, "spec1");
const room = manager.getRoom(create.roomId);
expect(room?.spectators.has("spec1")).toBe(false);
});
});
describe("listRooms", () => {
it("should return summaries of all rooms", () => {
manager.createRoom("stub", "player1");
manager.createRoom("stub", "player2");
const rooms = manager.listRooms();
expect(rooms.length).toBe(2);
expect(rooms[0].gameSlug).toBe("stub");
expect(rooms[0].status).toBe("waiting");
});
it("should filter by game type", () => {
manager.createRoom("stub", "player1");
const rooms = manager.listRooms("stub");
expect(rooms.length).toBe(1);
const empty = manager.listRooms("blackjack");
expect(empty.length).toBe(0);
});
});
describe("waiting room cleanup", () => {
it("should remove waiting rooms after the configured timeout", async () => {
const shortLivedManager = new RoomManager({ WAITING_CLEANUP_MS: 20 });
const create = shortLivedManager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
await new Promise(resolve => setTimeout(resolve, 35));
expect(shortLivedManager.getRoom(create.roomId)).toBeUndefined();
});
it("should refresh the waiting room timeout when the room is active", async () => {
const shortLivedManager = new RoomManager({ WAITING_CLEANUP_MS: 25 });
const create = shortLivedManager.createRoom("stub", "player1");
if (!create.ok) throw new Error("Failed to create room");
await new Promise(resolve => setTimeout(resolve, 15));
const spectatorJoin = shortLivedManager.joinRoom(create.roomId, "spectator1", "spectator");
expect(spectatorJoin.ok).toBe(true);
expect(shortLivedManager.getRoom(create.roomId)).toBeDefined();
await new Promise(resolve => setTimeout(resolve, 15));
expect(shortLivedManager.getRoom(create.roomId)).toBeDefined();
await new Promise(resolve => setTimeout(resolve, 20));
expect(shortLivedManager.getRoom(create.roomId)).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,327 @@
import mitt from "mitt";
import { gameRegistry } from "@shared/games/registry";
import type { Room, RoomSummary } from "./types";
import type { RoundSettlement } from "@shared/games/types";
const DEFAULT_ROOM_CONFIG = {
WAITING_CLEANUP_MS: 15 * 60_000,
FINISHED_CLEANUP_MS: 60_000,
PLAYING_MAX_MS: 30 * 60_000, // 30 minutes — safety net for stuck games
} as const;
type RoomManagerConfig = typeof DEFAULT_ROOM_CONFIG;
type ActionResult =
| { ok: true; state: unknown; gameOver: { winner: string | null; reason: string } | null; roundSettlements?: Record<string, RoundSettlement> }
| { ok: false; error: string };
type CreateResult = { ok: true; roomId: string } | { ok: false; error: string };
type JoinResult =
| { ok: true; joinedAs: "player" | "spectator"; started: boolean; readyToStart?: boolean }
| { ok: false; error: string };
type FillResult = { ok: true; readyToStart?: boolean } | { ok: false; error: string };
type RoomEvents = {
"room:created": { roomId: string; gameSlug: string; hostId: string };
"player:joined": { roomId: string; playerId: string; username: string; joinedAs: "player" | "spectator" };
"game:started": { roomId: string; spectatorView: unknown; playerViews: Map<string, unknown> };
"game:updated": { roomId: string; spectatorView: unknown; playerViews: Map<string, unknown> };
"game:ended": { roomId: string; winner: string | null; reason: string; payouts?: Record<string, number> };
"round:settled": { roomId: string; roundSettlements: Record<string, RoundSettlement> };
"player:left": { roomId: string; playerId: string };
"room:deleted": { roomId: string };
"room:list:changed": void;
};
export class RoomManager {
private rooms = new Map<string, Room>();
private cleanupTimers = new Map<string, Timer>();
private readonly config: RoomManagerConfig;
readonly emitter = mitt<RoomEvents>();
constructor(config: Partial<RoomManagerConfig> = {}) {
this.config = { ...DEFAULT_ROOM_CONFIG, ...config };
}
createRoom(gameSlug: string, hostId: string, options?: Record<string, unknown>): CreateResult {
const plugin = gameRegistry.get(gameSlug);
if (!plugin) return { ok: false, error: `Unknown game type: ${gameSlug}` };
const id = crypto.randomUUID();
const betAmount = typeof options?.betAmount === "number" && options.betAmount > 0 ? options.betAmount : 0;
const room: Room = {
id,
gameSlug,
host: hostId,
players: [hostId],
spectators: new Set(),
state: null,
status: "waiting",
createdAt: Date.now(),
options,
betAmount,
};
this.rooms.set(id, room);
this.refreshWaitingCleanup(id, room);
this.emitter.emit("room:created", { roomId: id, gameSlug, hostId });
this.emitter.emit("room:list:changed");
return { ok: true, roomId: id };
}
joinRoom(roomId: string, playerId: string, preferAs: "player" | "spectator", role?: string): JoinResult {
const room = this.rooms.get(roomId);
if (!room) return { ok: false, error: "Room not found" };
// Reconnecting player: must be checked before the in-progress spectator guard.
if (preferAs !== "spectator" && room.players.includes(playerId)) {
room.spectators.delete(playerId);
this.refreshWaitingCleanup(roomId, room);
return { ok: true, joinedAs: "player", started: room.status === "playing" };
}
if (preferAs === "spectator" || room.status !== "waiting") {
room.spectators.add(playerId);
this.refreshWaitingCleanup(roomId, room);
return { ok: true, joinedAs: "spectator", started: room.status === "playing" };
}
const plugin = gameRegistry.get(room.gameSlug)!;
const isAdmin = role === "admin";
if (room.players.length >= plugin.maxPlayers && !isAdmin) {
// Downgrade to spectator — room is full but still waiting, so game hasn't started
room.spectators.add(playerId);
return { ok: true, joinedAs: "spectator", started: false };
}
room.players.push(playerId);
if (room.players.length >= plugin.maxPlayers && !plugin.manualStart) {
// Defer start when bets are involved — GameServer handles async deduction first
if (room.betAmount > 0) {
this.refreshWaitingCleanup(roomId, room);
this.emitter.emit("room:list:changed");
return { ok: true, joinedAs: "player", started: false, readyToStart: true };
}
this.startGame(roomId);
return { ok: true, joinedAs: "player", started: true };
}
this.refreshWaitingCleanup(roomId, room);
this.emitter.emit("room:list:changed");
return { ok: true, joinedAs: "player", started: false };
}
handleAction(roomId: string, playerId: string, action: unknown): ActionResult {
const room = this.rooms.get(roomId);
if (!room) return { ok: false, error: "Room not found" };
if (room.status !== "playing") return { ok: false, error: "Game is not in progress" };
const plugin = gameRegistry.get(room.gameSlug)!;
// Spectator-to-player promotion for actions like "sit_down"
if (!room.players.includes(playerId)) {
if (room.spectators.has(playerId) && plugin.isSpectatorAction?.(action as any)) {
room.spectators.delete(playerId);
room.players.push(playerId);
} else {
return { ok: false, error: "You are not a player in this game" };
}
}
const result = plugin.handleAction(room.state, action, playerId);
if (!result.ok) return result;
room.state = result.state;
const gameOver = plugin.isGameOver?.(room.state) ?? null;
if (gameOver) {
room.status = "finished";
this.scheduleCleanup(roomId, this.config.FINISHED_CLEANUP_MS);
}
const spectatorView = plugin.getSpectatorView(room.state);
const playerViews = new Map<string, unknown>();
for (const pid of new Set(room.players)) {
playerViews.set(pid, plugin.getPlayerView(room.state, pid));
}
this.emitter.emit("game:updated", { roomId, spectatorView, playerViews });
// Emit round payouts for mid-game settlement (continuous-play games)
if (result.roundSettlements && !gameOver) {
this.emitter.emit("round:settled", { roomId, roundSettlements: result.roundSettlements });
}
if (gameOver) {
this.emitter.emit("game:ended", { roomId, winner: gameOver.winner, reason: gameOver.reason, payouts: gameOver.payouts });
this.emitter.emit("room:list:changed");
}
return { ok: true, state: room.state, gameOver, roundSettlements: result.roundSettlements };
}
leaveRoom(roomId: string, playerId: string): void {
const room = this.rooms.get(roomId);
if (!room) return;
room.spectators.delete(playerId);
const playerIdx = room.players.indexOf(playerId);
if (playerIdx !== -1) {
room.players.splice(playerIdx, 1);
if (room.status === "playing") {
const plugin = gameRegistry.get(room.gameSlug)!;
if (plugin.onPlayerDisconnect) {
room.state = plugin.onPlayerDisconnect(room.state, playerId);
const gameOver = plugin.isGameOver?.(room.state) ?? null;
if (gameOver) {
room.status = "finished";
this.scheduleCleanup(roomId, this.config.FINISHED_CLEANUP_MS);
this.emitter.emit("game:ended", { roomId, winner: gameOver.winner, reason: gameOver.reason, payouts: gameOver.payouts });
}
}
}
if (room.players.length === 0 && room.status === "waiting") {
this.deleteRoom(roomId);
return;
}
}
this.refreshWaitingCleanup(roomId, room);
this.emitter.emit("player:left", { roomId, playerId });
this.emitter.emit("room:list:changed");
}
/**
* Fills empty seats with the admin's own ID for solo testing.
* This means `createInitialState` will receive duplicate player IDs
* (e.g. ["alice", "alice"]). Plugin authors should be aware that
* solo-test mode produces non-unique player arrays.
*/
fillRoom(roomId: string, adminId: string): FillResult {
const room = this.rooms.get(roomId);
if (!room) return { ok: false, error: "Room not found" };
if (room.status !== "waiting") return { ok: false, error: "Game is not in waiting state" };
if (!room.players.includes(adminId)) return { ok: false, error: "You are not in this room" };
const plugin = gameRegistry.get(room.gameSlug)!;
while (room.players.length < plugin.maxPlayers) {
room.players.push(adminId);
}
// Defer start when bets are involved
if (room.betAmount > 0) {
this.refreshWaitingCleanup(roomId, room);
this.emitter.emit("room:list:changed");
return { ok: true, readyToStart: true };
}
this.startGame(roomId);
return { ok: true };
}
/** Initialize game state and transition room to playing. */
startGame(roomId: string): { ok: true } | { ok: false; error: string } {
const room = this.rooms.get(roomId);
if (!room) return { ok: false, error: "Room not found" };
if (room.status !== "waiting") return { ok: false, error: "Game is not in waiting state" };
const plugin = gameRegistry.get(room.gameSlug)!;
room.state = plugin.createInitialState(room.players, room.options);
room.status = "playing";
this.scheduleCleanup(roomId, this.config.PLAYING_MAX_MS);
const spectatorView = plugin.getSpectatorView(room.state);
const playerViews = new Map<string, unknown>();
for (const pid of new Set(room.players)) {
playerViews.set(pid, plugin.getPlayerView(room.state, pid));
}
this.emitter.emit("game:started", { roomId, spectatorView, playerViews });
this.emitter.emit("room:list:changed");
return { ok: true };
}
/** Remove a player from a waiting room (used when bet deduction fails). */
removePlayer(roomId: string, playerId: string): void {
const room = this.rooms.get(roomId);
if (!room || room.status !== "waiting") return;
const idx = room.players.indexOf(playerId);
if (idx !== -1) room.players.splice(idx, 1);
if (room.players.length === 0) {
this.deleteRoom(roomId);
return;
}
this.refreshWaitingCleanup(roomId, room);
this.emitter.emit("room:list:changed");
}
getRoom(roomId: string): Room | undefined {
return this.rooms.get(roomId);
}
listRooms(gameSlug?: string): RoomSummary[] {
const summaries: RoomSummary[] = [];
for (const room of this.rooms.values()) {
if (gameSlug && room.gameSlug !== gameSlug) continue;
const plugin = gameRegistry.get(room.gameSlug);
summaries.push({
id: room.id,
gameSlug: room.gameSlug,
gameName: plugin?.name ?? room.gameSlug,
host: room.host,
playerCount: room.players.length,
maxPlayers: plugin?.maxPlayers ?? 0,
spectatorCount: room.spectators.size,
status: room.status,
betAmount: room.betAmount,
});
}
return summaries;
}
getPlayerView(roomId: string, playerId: string): unknown {
const room = this.rooms.get(roomId);
if (!room || !room.state) return null;
const plugin = gameRegistry.get(room.gameSlug)!;
return plugin.getPlayerView(room.state, playerId);
}
getSpectatorView(roomId: string): unknown {
const room = this.rooms.get(roomId);
if (!room || !room.state) return null;
const plugin = gameRegistry.get(room.gameSlug)!;
return plugin.getSpectatorView(room.state);
}
private scheduleCleanup(roomId: string, ms: number): void {
this.clearCleanup(roomId);
const timer = setTimeout(() => this.deleteRoom(roomId), ms);
this.cleanupTimers.set(roomId, timer);
}
private refreshWaitingCleanup(roomId: string, room: Room): void {
if (room.status !== "waiting") return;
this.scheduleCleanup(roomId, this.config.WAITING_CLEANUP_MS);
}
private clearCleanup(roomId: string): void {
const existing = this.cleanupTimers.get(roomId);
if (existing) {
clearTimeout(existing);
this.cleanupTimers.delete(roomId);
}
}
private deleteRoom(roomId: string): void {
this.clearCleanup(roomId);
if (this.rooms.delete(roomId)) {
this.emitter.emit("room:deleted", { roomId });
this.emitter.emit("room:list:changed");
}
}
}

65
api/src/games/types.ts Normal file
View File

@@ -0,0 +1,65 @@
import type { RoundSettlement } from "@shared/games/types";
import { z } from "zod";
export interface Room {
id: string;
gameSlug: string;
host: string;
players: string[];
spectators: Set<string>;
state: unknown;
status: "waiting" | "playing" | "finished";
createdAt: number;
options?: Record<string, unknown>;
betAmount: number;
/** Guard against double bet-deduction when two joins race */
betsPending?: boolean;
}
export interface RoomSummary {
id: string;
gameSlug: string;
gameName: string;
host: string;
playerCount: number;
maxPlayers: number;
spectatorCount: number;
status: "waiting" | "playing" | "finished";
betAmount: number;
}
export interface PlayerInfo {
discordId: string;
username: string;
}
export const GameWsClientSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("CREATE_ROOM"), gameType: z.string(), options: z.looseObject({}).optional() }),
z.object({
type: z.literal("JOIN_ROOM"),
roomId: z.string(),
preferAs: z.enum(["player", "spectator"]),
role: z.enum(["player", "admin"]).optional(),
}),
z.object({ type: z.literal("LEAVE_ROOM"), roomId: z.string() }),
// Use looseObject for GAME_ACTION to avoid Zod bug with record()
z.object({ type: z.literal("GAME_ACTION"), roomId: z.string(), action: z.looseObject({}, { message: "Invalid action" }) }),
z.object({ type: z.literal("FILL_ROOM"), roomId: z.string() }),
z.object({ type: z.literal("START_GAME"), roomId: z.string() }),
]);
export type GameWsClientMessage = z.infer<typeof GameWsClientSchema>;
export type GameWsServerMessage =
| { type: "ROOM_LIST_UPDATE"; rooms: RoomSummary[] }
| { type: "GAME_STATE"; roomId: string; state: unknown }
| { type: "GAME_UPDATE"; roomId: string; state: unknown }
| { type: "PLAYER_JOINED"; roomId: string; player: PlayerInfo; joinedAs: "player" | "spectator" }
| { type: "PLAYER_LEFT"; roomId: string; playerId: string }
| { type: "GAME_STARTED"; roomId: string; state: unknown }
| { type: "GAME_ENDED"; roomId: string; winner: string | null; reason: string; payout?: { amount: number; refunded?: boolean } }
| { type: "ROOM_CREATED"; roomId: string; gameSlug: string }
| { type: "JOIN_RESULT"; roomId: string; joinedAs: "player" | "spectator"; roomStatus: "waiting" | "playing" | "finished"; players: PlayerInfo[]; spectators: PlayerInfo[]; state?: unknown; roomOptions?: { betAmount?: number; timeControl?: string } }
| { type: "ROUND_SETTLED"; roomId: string; settlements: Record<string, RoundSettlement> }
| { type: "SESSION_REPLACED"; roomId: string }
| { type: "ERROR"; message: string };

View File

@@ -0,0 +1,103 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
const findFirst = mock(async () => ({ id: 123n }));
mock.module("@shared/db/DrizzleClient", () => ({
DrizzleClient: {
query: {
users: {
findFirst,
},
},
},
}));
mock.module("@shared/lib/logger", () => ({
logger: {
error: () => { },
info: () => { },
warn: () => { },
debug: () => { },
},
}));
import { authRoutes, getSession } from "./auth.routes";
describe("Auth Routes", () => {
let fetchSpy: ReturnType<typeof spyOn> | null = null;
beforeEach(() => {
process.env.DISCORD_CLIENT_ID = "client-id";
process.env.DISCORD_CLIENT_SECRET = "client-secret";
process.env.SESSION_SECRET = "session-secret";
process.env.PANEL_BASE_URL = "http://localhost:3000";
process.env.ADMIN_USER_IDS = "123";
findFirst.mockClear();
});
afterEach(() => {
fetchSpy?.mockRestore();
fetchSpy = null;
});
it("creates a signed session cookie during OAuth callback", async () => {
const loginUrl = new URL("http://localhost/auth/discord?return_to=http://localhost:5173/admin");
const loginRes = await authRoutes.handler({
req: new Request(loginUrl, { method: "GET" }),
url: loginUrl,
method: "GET",
pathname: "/auth/discord",
});
expect(loginRes?.status).toBe(302);
const redirectLocation = loginRes?.headers.get("Location");
expect(redirectLocation).not.toBeNull();
const state = new URL(redirectLocation!).searchParams.get("state");
expect(state).not.toBeNull();
fetchSpy = spyOn(globalThis, "fetch");
fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: "discord-token" }), { status: 200 }));
fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({
id: "123",
username: "aurora-admin",
avatar: null,
}), { status: 200 }));
const callbackUrl = new URL(`http://localhost/auth/callback?code=oauth-code&state=${encodeURIComponent(state!)}`);
const callbackRes = await authRoutes.handler({
req: new Request(callbackUrl, { method: "GET" }),
url: callbackUrl,
method: "GET",
pathname: "/auth/callback",
});
expect(callbackRes?.status).toBe(302);
expect(callbackRes?.headers.get("Location")).toBe("/admin");
const setCookie = callbackRes?.headers.get("Set-Cookie");
expect(setCookie).toContain("aurora_session=");
const sessionCookie = setCookie!.split(";")[0]!;
const session = getSession(new Request("http://localhost/api/me", {
headers: { cookie: sessionCookie },
}));
expect(session).toEqual({
discordId: "123",
username: "aurora-admin",
avatar: null,
role: "admin",
expiresAt: expect.any(Number),
});
});
it("rejects tampered session cookies", () => {
const session = getSession(new Request("http://localhost/api/me", {
headers: { cookie: "aurora_session=not-a-valid-token" },
}));
expect(session).toBeNull();
});
});

View File

@@ -3,22 +3,39 @@
* Handles login flow, callback, logout, and session management.
*/
import { Buffer } from "node:buffer";
import { createHmac, timingSafeEqual } from "node:crypto";
import type { RouteContext, RouteModule } from "./types";
import { jsonResponse, errorResponse } from "./utils";
import { logger } from "@shared/lib/logger";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { users } from "@shared/db/schema";
import { eq } from "drizzle-orm";
// In-memory session store: token → { discordId, username, avatar, expiresAt }
// Signed session payload stored in the aurora_session cookie.
export interface Session {
discordId: string;
username: string;
avatar: string | null;
role: "admin" | "player";
expiresAt: number;
}
const sessions = new Map<string, Session>();
const redirects = new Map<string, string>(); // redirect token -> return_to URL
interface SessionTokenPayload extends Session {
v: 1;
}
interface OAuthStatePayload {
exp: number;
returnTo: string;
v: 1;
}
const COOKIE_NAME = "aurora_session";
const SESSION_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
const OAUTH_STATE_MAX_AGE = 10 * 60 * 1000; // 10 minutes
const TOKEN_NAMESPACE = "aurora.auth";
const TOKEN_VERSION = "v1";
function getEnv(key: string): string {
const val = process.env[key];
@@ -26,15 +43,70 @@ function getEnv(key: string): string {
return val;
}
function getSessionSecret(required: boolean = false): string | null {
const secret = process.env.SESSION_SECRET ?? process.env.DISCORD_CLIENT_SECRET ?? null;
if (!secret && required) {
throw new Error("Missing env: SESSION_SECRET or DISCORD_CLIENT_SECRET");
}
return secret;
}
function requireSessionSecret(): string {
return getSessionSecret(true)!;
}
function getAdminIds(): string[] {
const raw = process.env.ADMIN_USER_IDS ?? "";
return raw.split(",").map(s => s.trim()).filter(Boolean);
}
function generateToken(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
function encodeBase64Url(value: string): string {
return Buffer.from(value, "utf8").toString("base64url");
}
function decodeBase64Url(value: string): string {
return Buffer.from(value, "base64url").toString("utf8");
}
function signValue(kind: string, encodedPayload: string, secret: string): string {
return createHmac("sha256", secret)
.update(`${TOKEN_NAMESPACE}.${kind}.${encodedPayload}`)
.digest("base64url");
}
function serializeSignedToken(kind: string, payload: SessionTokenPayload | OAuthStatePayload, secret: string): string {
const encodedPayload = encodeBase64Url(JSON.stringify(payload));
const signature = signValue(kind, encodedPayload, secret);
return `${TOKEN_VERSION}.${encodedPayload}.${signature}`;
}
function parseSignedToken<T>(token: string | undefined, kind: string): T | null {
if (!token) return null;
const secret = getSessionSecret();
if (!secret) return null;
const parts = token.split(".");
if (parts.length !== 3) return null;
const version = parts[0];
const encodedPayload = parts[1];
const providedSignature = parts[2];
if (version !== TOKEN_VERSION) return null;
if (!encodedPayload || !providedSignature) return null;
const expectedSignature = signValue(kind, encodedPayload, secret);
const providedBuffer = Buffer.from(providedSignature);
const expectedBuffer = Buffer.from(expectedSignature);
if (providedBuffer.length !== expectedBuffer.length) return null;
if (!timingSafeEqual(providedBuffer, expectedBuffer)) return null;
try {
return JSON.parse(decodeBase64Url(encodedPayload)) as T;
} catch {
return null;
}
}
function getBaseUrl(): string {
@@ -51,18 +123,65 @@ function parseCookies(header: string | null): Record<string, string> {
return cookies;
}
function sanitizeReturnTo(rawReturnTo: string | null, baseUrl: string): string {
if (!rawReturnTo || rawReturnTo.length > 1024) return "/";
try {
if (rawReturnTo.startsWith("/") && !rawReturnTo.startsWith("//")) {
return rawReturnTo;
}
const parsed = new URL(rawReturnTo, baseUrl);
const allowedBase = new URL(baseUrl);
const isLocalhostRedirect = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
if (parsed.origin === allowedBase.origin || isLocalhostRedirect) {
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
return "/";
}
return "/";
}
function buildCookieAttributes(maxAgeSeconds?: number): string {
const attrs = [
"Path=/",
"HttpOnly",
"SameSite=Lax",
];
try {
if (new URL(getBaseUrl()).protocol === "https:") {
attrs.push("Secure");
}
} catch {
// Ignore invalid PANEL_BASE_URL here; handlers that need it will fail explicitly.
}
if (typeof maxAgeSeconds === "number") {
attrs.push(`Max-Age=${maxAgeSeconds}`);
}
return attrs.join("; ");
}
/** Get session from request cookie */
export function getSession(req: Request): Session | null {
const cookies = parseCookies(req.headers.get("cookie"));
const token = cookies["aurora_session"];
if (!token) return null;
const session = sessions.get(token);
if (!session) return null;
if (Date.now() > session.expiresAt) {
sessions.delete(token);
return null;
}
return session;
const payload = parseSignedToken<SessionTokenPayload>(cookies[COOKIE_NAME], "session");
if (!payload || payload.v !== 1) return null;
if (Date.now() > payload.expiresAt) return null;
return {
discordId: payload.discordId,
username: payload.username,
avatar: payload.avatar,
role: payload.role,
expiresAt: payload.expiresAt,
};
}
/** Check if request is authenticated as admin */
@@ -80,20 +199,22 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
const baseUrl = getBaseUrl();
const redirectUri = encodeURIComponent(`${baseUrl}/auth/callback`);
const scope = "identify+email";
const secret = requireSessionSecret();
// Store return_to URL if provided
const returnTo = ctx.url.searchParams.get("return_to") || "/";
const redirectToken = generateToken();
redirects.set(redirectToken, returnTo);
// Store return_to URL in signed OAuth state
const returnTo = sanitizeReturnTo(ctx.url.searchParams.get("return_to"), baseUrl);
const state = serializeSignedToken("oauth", {
exp: Date.now() + OAUTH_STATE_MAX_AGE,
returnTo,
v: 1,
}, secret);
const url = `https://discord.com/oauth2/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=${scope}`;
const url = `https://discord.com/oauth2/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=${scope}&state=${encodeURIComponent(state)}`;
// Set a temporary cookie with the redirect token
return new Response(null, {
status: 302,
headers: {
Location: url,
"Set-Cookie": `aurora_redirect=${redirectToken}; Path=/; Max-Age=600; SameSite=Lax`,
},
});
} catch (e) {
@@ -112,8 +233,13 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
const clientSecret = getEnv("DISCORD_CLIENT_SECRET");
const baseUrl = getBaseUrl();
const redirectUri = `${baseUrl}/auth/callback`;
const secret = requireSessionSecret();
const statePayload = parseSignedToken<OAuthStatePayload>(ctx.url.searchParams.get("state") ?? undefined, "oauth");
if (!statePayload || statePayload.v !== 1 || Date.now() > statePayload.exp) {
return errorResponse("Invalid OAuth state", 400);
}
// Exchange code for token
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -144,49 +270,41 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
const user = await userRes.json() as { id: string; username: string; avatar: string | null };
// Check allowlist
const adminIds = getAdminIds();
if (adminIds.length > 0 && !adminIds.includes(user.id)) {
logger.warn("auth", `Unauthorized login attempt by ${user.username} (${user.id})`);
// Check enrollment — user must exist in the users table
const dbUser = await DrizzleClient.query.users.findFirst({
where: eq(users.id, BigInt(user.id)),
});
if (!dbUser) {
logger.info("auth", `Non-enrolled login attempt by ${user.username} (${user.id})`);
return new Response(
`<html><body><h1>Access Denied</h1><p>Your Discord account is not authorized.</p></body></html>`,
`<html><body><h1>Not Enrolled</h1><p>You need to use the Aurora bot in Discord before you can access this panel.</p><a href="/">Go back</a></body></html>`,
{ status: 403, headers: { "Content-Type": "text/html" } }
);
}
// Create session
const token = generateToken();
sessions.set(token, {
// Determine role
const adminIds = getAdminIds();
const role: "admin" | "player" = adminIds.includes(user.id) ? "admin" : "player";
// Create signed session cookie
const sessionToken = serializeSignedToken("session", {
discordId: user.id,
username: user.username,
avatar: user.avatar,
role,
expiresAt: Date.now() + SESSION_MAX_AGE,
});
v: 1,
}, secret);
logger.info("auth", `Admin login: ${user.username} (${user.id})`);
// Get return_to URL from redirect token cookie
const cookies = parseCookies(ctx.req.headers.get("cookie"));
const redirectToken = cookies["aurora_redirect"];
let returnTo = redirectToken && redirects.get(redirectToken) ? redirects.get(redirectToken)! : "/";
if (redirectToken) redirects.delete(redirectToken);
// Only allow redirects to localhost or relative paths (prevent open redirect)
try {
const parsed = new URL(returnTo, baseUrl);
if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
returnTo = "/";
}
} catch {
returnTo = "/";
}
logger.info("auth", `Login: ${user.username} (${user.id}) as ${role}`);
// Redirect to panel with session cookie
return new Response(null, {
status: 302,
headers: {
Location: returnTo,
"Set-Cookie": `aurora_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE / 1000}`,
Location: sanitizeReturnTo(statePayload.returnTo, baseUrl),
"Set-Cookie": `${COOKIE_NAME}=${sessionToken}; ${buildCookieAttributes(SESSION_MAX_AGE / 1000)}`,
},
});
} catch (e) {
@@ -197,14 +315,10 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
// POST /auth/logout — clear session
if (pathname === "/auth/logout" && method === "POST") {
const cookies = parseCookies(ctx.req.headers.get("cookie"));
const token = cookies["aurora_session"];
if (token) sessions.delete(token);
return new Response(null, {
status: 200,
headers: {
"Set-Cookie": "aurora_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
"Set-Cookie": `${COOKIE_NAME}=; ${buildCookieAttributes(0)}`,
"Content-Type": "application/json",
},
});
@@ -213,13 +327,15 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
// GET /auth/me — return current session info
if (pathname === "/auth/me" && method === "GET") {
const session = getSession(ctx.req);
if (!session) return jsonResponse({ authenticated: false }, 401);
if (!session) return jsonResponse({ authenticated: false, enrolled: true });
return jsonResponse({
authenticated: true,
enrolled: true,
user: {
discordId: session.discordId,
username: session.username,
avatar: session.avatar,
role: session.role,
},
});
}

View File

@@ -0,0 +1,114 @@
import { beforeEach, describe, expect, it, mock } from "bun:test";
let currentSession: { discordId: string; username: string; role: "admin" | "player"; expiresAt: number } | null = null;
mock.module("./auth.routes", () => ({
authRoutes: { name: "auth", handler: () => null },
getSession: () => currentSession,
}));
mock.module("./health.routes", () => ({
healthRoutes: {
name: "health",
handler: ({ pathname }: { pathname: string }) =>
pathname === "/api/health"
? Response.json({ status: "ok" }, { status: 200 })
: null,
},
}));
mock.module("./stats.routes", () => ({
statsRoutes: {
name: "stats",
handler: ({ pathname }: { pathname: string }) =>
pathname === "/api/stats"
? Response.json({ ok: true }, { status: 200 })
: null,
},
}));
mock.module("./actions.routes", () => ({ actionsRoutes: { name: "actions", handler: () => null } }));
mock.module("./quests.routes", () => ({ questsRoutes: { name: "quests", handler: () => null } }));
mock.module("./settings.routes", () => ({ settingsRoutes: { name: "settings", handler: () => null } }));
mock.module("./guild-settings.routes", () => ({ guildSettingsRoutes: { name: "guild-settings", handler: () => null } }));
mock.module("./items.routes", () => ({ itemsRoutes: { name: "items", handler: () => null } }));
mock.module("./classes.routes", () => ({ classesRoutes: { name: "classes", handler: () => null } }));
mock.module("./moderation.routes", () => ({ moderationRoutes: { name: "moderation", handler: () => null } }));
mock.module("./transactions.routes", () => ({ transactionsRoutes: { name: "transactions", handler: () => null } }));
mock.module("./lootdrops.routes", () => ({ lootdropsRoutes: { name: "lootdrops", handler: () => null } }));
mock.module("./assets.routes", () => ({ assetsRoutes: { name: "assets", handler: () => null } }));
mock.module("@shared/modules/user/user.service", () => ({
userService: {
getUserById: async (id: string) => ({ id, username: `user-${id}` }),
},
}));
mock.module("@shared/modules/inventory/inventory.service", () => ({
inventoryService: {
getInventory: async (id: string) => [{ userId: id, itemId: 1, quantity: 1n }],
},
}));
mock.module("@shared/lib/logger", () => ({
logger: {
error: () => { },
info: () => { },
warn: () => { },
debug: () => { },
},
}));
import { handleRequest } from "./index";
describe("Route Authorization", () => {
beforeEach(() => {
currentSession = null;
});
it("rejects unauthenticated protected API requests", async () => {
const url = new URL("http://localhost/api/users/123");
const res = await handleRequest(new Request(url, { method: "GET" }), url);
expect(res?.status).toBe(401);
});
it("blocks players from admin user routes", async () => {
currentSession = {
discordId: "123",
username: "player",
role: "player",
expiresAt: Date.now() + 60_000,
};
const url = new URL("http://localhost/api/users/456");
const res = await handleRequest(new Request(url, { method: "GET" }), url);
expect(res?.status).toBe(403);
});
it("allows players to access self-service API routes", async () => {
currentSession = {
discordId: "123",
username: "player",
role: "player",
expiresAt: Date.now() + 60_000,
};
const url = new URL("http://localhost/api/me/inventory");
const res = await handleRequest(new Request(url, { method: "GET" }), url);
expect(res?.status).toBe(200);
});
it("allows admins to access admin user routes", async () => {
currentSession = {
discordId: "1",
username: "admin",
role: "admin",
expiresAt: Date.now() + 60_000,
};
const url = new URL("http://localhost/api/users/456");
const res = await handleRequest(new Request(url, { method: "GET" }), url);
expect(res?.status).toBe(200);
});
});

View File

@@ -4,7 +4,7 @@
*/
import type { RouteContext, RouteModule } from "./types";
import { authRoutes, isAuthenticated } from "./auth.routes";
import { authRoutes, getSession } from "./auth.routes";
import { healthRoutes } from "./health.routes";
import { statsRoutes } from "./stats.routes";
import { actionsRoutes } from "./actions.routes";
@@ -70,9 +70,18 @@ export async function handleRequest(req: Request, url: URL): Promise<Response |
// For API routes, enforce authentication
if (ctx.pathname.startsWith("/api/")) {
if (!isAuthenticated(req)) {
const session = getSession(req);
if (!session) {
return errorResponse("Unauthorized", 401);
}
// Player routes are explicitly allow-listed. Everything else is admin-only.
const playerAllowedPrefixes = ["/api/stats", "/api/health", "/api/me"];
const isPlayerAllowed = playerAllowedPrefixes.some(p => ctx.pathname.startsWith(p));
if (session.role === "player" && !isPlayerAllowed) {
return errorResponse("Admin access required", 403);
}
}
// Try protected routes

View File

@@ -110,7 +110,7 @@ export const UpdateUserSchema = z.object({
dailyStreak: z.coerce.number().int().min(0).optional(),
isActive: z.boolean().optional(),
settings: z.record(z.string(), z.any()).optional(),
classId: z.union([z.string(), z.number()]).optional(),
classId: z.union([z.string(), z.number()]).nullable().optional(),
});
/**

View File

@@ -0,0 +1,140 @@
import { beforeEach, describe, expect, it, mock } from "bun:test";
let currentSession: { discordId: string; username: string; role: "admin" | "player"; expiresAt: number } | null = null;
const getUserById = mock(async (id: string) => ({
id,
username: id === "123" ? "player-one" : "user",
level: 5,
xp: 100n,
balance: 250n,
className: null,
}));
const updateUser = mock(async (id: string, data: Record<string, unknown>) => ({
id,
...data,
}));
const getInventory = mock(async (id: string) => [{ userId: id, itemId: 1, quantity: 2n }]);
const addItem = mock(async (userId: string, itemId: number, quantity: bigint) => ({ userId, itemId, quantity }));
const removeItem = mock(async () => undefined);
mock.module("./auth.routes", () => ({
getSession: () => currentSession,
}));
mock.module("@shared/modules/user/user.service", () => ({
userService: {
getUserById,
updateUser,
},
}));
mock.module("@shared/modules/inventory/inventory.service", () => ({
inventoryService: {
getInventory,
addItem,
removeItem,
},
}));
mock.module("@shared/lib/logger", () => ({
logger: {
error: () => { },
info: () => { },
warn: () => { },
debug: () => { },
},
}));
import { usersRoutes } from "./users.routes";
describe("Users Routes", () => {
beforeEach(() => {
currentSession = {
discordId: "123",
username: "player",
role: "player",
expiresAt: Date.now() + 60_000,
};
getUserById.mockClear();
updateUser.mockClear();
getInventory.mockClear();
addItem.mockClear();
removeItem.mockClear();
});
it("serves the authenticated user through /api/me", async () => {
const url = new URL("http://localhost/api/me");
const res = await usersRoutes.handler({
req: new Request(url, { method: "GET" }),
url,
method: "GET",
pathname: "/api/me",
});
expect(res?.status).toBe(200);
expect(getUserById).toHaveBeenCalledWith("123");
});
it("serves the authenticated user's inventory through /api/me/inventory", async () => {
const url = new URL("http://localhost/api/me/inventory");
const res = await usersRoutes.handler({
req: new Request(url, { method: "GET" }),
url,
method: "GET",
pathname: "/api/me/inventory",
});
expect(res?.status).toBe(200);
expect(getInventory).toHaveBeenCalledWith("123");
});
it("validates user updates before calling the service", async () => {
const url = new URL("http://localhost/api/users/123");
const res = await usersRoutes.handler({
req: new Request(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ level: -1 }),
}),
url,
method: "PUT",
pathname: "/api/users/123",
});
expect(res?.status).toBe(400);
expect(updateUser).not.toHaveBeenCalled();
});
it("validates inventory additions before calling the service", async () => {
const url = new URL("http://localhost/api/users/123/inventory");
const res = await usersRoutes.handler({
req: new Request(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ itemId: 1, quantity: 0 }),
}),
url,
method: "POST",
pathname: "/api/users/123/inventory",
});
expect(res?.status).toBe(400);
expect(addItem).not.toHaveBeenCalled();
});
it("validates inventory removal query params before calling the service", async () => {
const url = new URL("http://localhost/api/users/123/inventory/1?amount=0");
const res = await usersRoutes.handler({
req: new Request(url, { method: "DELETE" }),
url,
method: "DELETE",
pathname: "/api/users/123/inventory/1",
});
expect(res?.status).toBe(400);
expect(removeItem).not.toHaveBeenCalled();
});
});

View File

@@ -8,16 +8,19 @@ import {
jsonResponse,
errorResponse,
parseBody,
parseIdFromPath,
parseQuery,
parseStringIdFromPath,
withErrorHandling
} from "./utils";
import { UpdateUserSchema, InventoryAddSchema } from "./schemas";
import { InventoryAddSchema, InventoryRemoveQuerySchema, UpdateUserSchema, UserQuerySchema } from "./schemas";
import { getSession } from "./auth.routes";
/**
* Users routes handler.
*
* Endpoints:
* - GET /api/me - Get current authenticated user
* - GET /api/me/inventory - Get current authenticated user's inventory
* - GET /api/users - List users with filters
* - GET /api/users/:id - Get single user
* - PUT /api/users/:id - Update user
@@ -30,6 +33,37 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
// Only handle requests to /api/users*
if (!pathname.startsWith("/api/users")) {
if (pathname === "/api/me" && method === "GET") {
return withErrorHandling(async () => {
const session = getSession(req);
if (!session) {
return errorResponse("Unauthorized", 401);
}
const { userService } = await import("@shared/modules/user/user.service");
const user = await userService.getUserById(session.discordId);
if (!user) {
return errorResponse("User not found", 404);
}
return jsonResponse(user);
}, "fetch current user");
}
if (pathname === "/api/me/inventory" && method === "GET") {
return withErrorHandling(async () => {
const session = getSession(req);
if (!session) {
return errorResponse("Unauthorized", 401);
}
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
const inventory = await inventoryService.getInventory(session.discordId);
return jsonResponse({ inventory });
}, "fetch current user inventory");
}
return null;
}
@@ -55,12 +89,12 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
const { users } = await import("@shared/db/schema");
const { DrizzleClient } = await import("@shared/db/DrizzleClient");
const { ilike, desc, asc, sql } = await import("drizzle-orm");
const queryParams = parseQuery(url, UserQuerySchema);
if (queryParams instanceof Response) {
return queryParams;
}
const search = url.searchParams.get("search") || undefined;
const sortBy = url.searchParams.get("sortBy") || "balance";
const sortOrder = url.searchParams.get("sortOrder") || "desc";
const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")!) : 50;
const offset = url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset")!) : 0;
const { search, sortBy, sortOrder, limit, offset } = queryParams;
let query = DrizzleClient.select().from(users);
@@ -146,7 +180,10 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
return withErrorHandling(async () => {
const { userService } = await import("@shared/modules/user/user.service");
const data = await req.json() as Record<string, any>;
const parsed = await parseBody(req, UpdateUserSchema);
if (parsed instanceof Response) {
return parsed;
}
const existing = await userService.getUserById(id);
if (!existing) {
@@ -155,14 +192,16 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
// Build update data (only allow safe fields)
const updateData: any = {};
if (data.username !== undefined) updateData.username = data.username;
if (data.balance !== undefined) updateData.balance = BigInt(data.balance);
if (data.xp !== undefined) updateData.xp = BigInt(data.xp);
if (data.level !== undefined) updateData.level = parseInt(data.level);
if (data.dailyStreak !== undefined) updateData.dailyStreak = parseInt(data.dailyStreak);
if (data.isActive !== undefined) updateData.isActive = Boolean(data.isActive);
if (data.settings !== undefined) updateData.settings = data.settings;
if (data.classId !== undefined) updateData.classId = BigInt(data.classId);
if (parsed.username !== undefined) updateData.username = parsed.username;
if (parsed.balance !== undefined) updateData.balance = BigInt(parsed.balance);
if (parsed.xp !== undefined) updateData.xp = BigInt(parsed.xp);
if (parsed.level !== undefined) updateData.level = parsed.level;
if (parsed.dailyStreak !== undefined) updateData.dailyStreak = parsed.dailyStreak;
if (parsed.isActive !== undefined) updateData.isActive = parsed.isActive;
if (parsed.settings !== undefined) updateData.settings = parsed.settings;
if (parsed.classId !== undefined) {
updateData.classId = parsed.classId === null ? null : BigInt(parsed.classId);
}
const updatedUser = await userService.updateUser(id, updateData);
return jsonResponse({ success: true, user: updatedUser });
@@ -215,13 +254,12 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
return withErrorHandling(async () => {
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
const data = await req.json() as Record<string, any>;
if (!data.itemId || !data.quantity) {
return errorResponse("Missing required fields: itemId, quantity", 400);
const parsed = await parseBody(req, InventoryAddSchema);
if (parsed instanceof Response) {
return parsed;
}
const entry = await inventoryService.addItem(id, data.itemId, BigInt(data.quantity));
const entry = await inventoryService.addItem(id, parsed.itemId, BigInt(parsed.quantity));
return jsonResponse({ success: true, entry }, 201);
}, "add item to inventory");
}
@@ -245,11 +283,12 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
return withErrorHandling(async () => {
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
const queryParams = parseQuery(url, InventoryRemoveQuerySchema);
if (queryParams instanceof Response) {
return queryParams;
}
const amount = url.searchParams.get("amount");
const quantity = amount ? BigInt(amount) : 1n;
await inventoryService.removeItem(userId, itemId, quantity);
await inventoryService.removeItem(userId, itemId, BigInt(queryParams.amount));
return new Response(null, { status: 204 });
}, "remove item from inventory");
}

View File

@@ -135,8 +135,7 @@ mock.module("@shared/lib/utils", () => ({
// --- Mock Auth (bypass authentication) ---
mock.module("./routes/auth.routes", () => ({
authRoutes: { name: "auth", handler: () => null },
isAuthenticated: () => true,
getSession: () => ({ discordId: "123", username: "testuser", expiresAt: Date.now() + 3600000 }),
getSession: () => ({ discordId: "123", username: "testuser", role: "admin", expiresAt: Date.now() + 3600000 }),
}));
// --- Mock Logger ---

View File

@@ -113,8 +113,7 @@ mock.module("bun", () => {
// Mock auth (bypass authentication)
mock.module("./routes/auth.routes", () => ({
authRoutes: { name: "auth", handler: () => null },
isAuthenticated: () => true,
getSession: () => ({ discordId: "123", username: "testuser", expiresAt: Date.now() + 3600000 }),
getSession: () => ({ discordId: "123", username: "testuser", role: "admin", expiresAt: Date.now() + 3600000 }),
}));
// Import createWebServer after mocks

View File

@@ -1,4 +1,4 @@
import { describe, test, expect, afterAll, mock } from "bun:test";
import { beforeEach, describe, test, expect, afterAll, mock } from "bun:test";
import type { WebServerInstance } from "./server";
interface MockBotStats {
@@ -66,11 +66,17 @@ mock.module("@shared/lib/config", () => ({
}
}));
// 4. Mock auth (bypass authentication for testing)
let currentSession: { discordId: string; username: string; role: "admin" | "player"; expiresAt: number } | null = {
discordId: "123",
username: "admin-user",
role: "admin",
expiresAt: Date.now() + 3600000,
};
// 4. Mock auth with a mutable session so tests can exercise authz paths.
mock.module("./routes/auth.routes", () => ({
authRoutes: { name: "auth", handler: () => null },
isAuthenticated: () => true,
getSession: () => ({ discordId: "123", username: "testuser", expiresAt: Date.now() + 3600000 }),
getSession: () => currentSession,
}));
// 5. Mock BotClient (used by stats helper for maintenanceMode)
@@ -91,37 +97,55 @@ describe("WebServer Security & Limits", () => {
const hostname = "127.0.0.1";
let serverInstance: WebServerInstance | null = null;
beforeEach(() => {
currentSession = {
discordId: "123",
username: "admin-user",
role: "admin",
expiresAt: Date.now() + 3600000,
};
});
afterAll(async () => {
if (serverInstance) {
await serverInstance.stop();
}
});
test("should reject more than 10 concurrent WebSocket connections", async () => {
test("should reject unauthorized websocket requests", async () => {
serverInstance = await createWebServer({ port, hostname });
const wsUrl = `ws://${hostname}:${port}/ws`;
const sockets: WebSocket[] = [];
currentSession = null;
try {
// Attempt to open 12 connections (limit is 10)
for (let i = 0; i < 12; i++) {
const ws = new WebSocket(wsUrl);
sockets.push(ws);
await new Promise(resolve => setTimeout(resolve, 5));
}
const response = await fetch(`http://${hostname}:${port}/ws`);
const body = await response.text();
// Give connections time to settle
await new Promise(resolve => setTimeout(resolve, 800));
expect(response.status).toBe(401);
expect(body).toBe("Unauthorized");
});
const pendingCount = serverInstance.server.pendingWebSockets;
expect(pendingCount).toBeLessThanOrEqual(10);
} finally {
sockets.forEach(s => {
if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING) {
s.close();
}
});
test("should accept websocket requests for authenticated sessions", async () => {
if (!serverInstance) {
serverInstance = await createWebServer({ port, hostname });
}
const ws = new WebSocket(`ws://${hostname}:${port}/ws`);
const opened = await new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => resolve(false), 1000);
ws.addEventListener("open", () => {
clearTimeout(timeout);
resolve(true);
});
ws.addEventListener("error", () => {
clearTimeout(timeout);
resolve(false);
});
});
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close();
}
expect(opened).toBe(true);
});
test("should return 200 for health check", async () => {
@@ -135,15 +159,30 @@ describe("WebServer Security & Limits", () => {
});
describe("Administrative Actions", () => {
test("should allow administrative actions without token", async () => {
test("should allow administrative actions for admin sessions", async () => {
const response = await fetch(`http://${hostname}:${port}/api/actions/reload-commands`, {
method: "POST"
});
// Should be 200 (OK) or 500 (if underlying service fails, but NOT 401)
expect(response.status).not.toBe(401);
expect(response.status).toBe(200);
});
test("should reject administrative actions for player sessions", async () => {
currentSession = {
discordId: "456",
username: "player-user",
role: "player",
expiresAt: Date.now() + 3600000,
};
const response = await fetch(`http://${hostname}:${port}/api/actions/reload-commands`, {
method: "POST"
});
expect(response.status).toBe(403);
const data = await response.json() as { error: string };
expect(data.error).toBe("Admin access required");
});
test("should reject maintenance mode with invalid payload", async () => {
const response = await fetch(`http://${hostname}:${port}/api/actions/maintenance-mode`, {
method: "POST",

View File

@@ -2,43 +2,36 @@
* @fileoverview API server factory module.
* Exports a function to create and start the API server.
* This allows the server to be started in-process from the main application.
*
*
* Routes are organized into modular files in the ./routes directory.
* Each route module handles its own validation, business logic, and responses.
*/
import { serve, file } from "bun";
import type { ServerWebSocket } from "bun";
import { logger } from "@shared/lib/logger";
import { handleRequest } from "./routes";
import { getFullDashboardStats } from "./routes/stats.helper";
import { join } from "path";
import { gameServer } from "./games/GameServer";
import type { WsConnectionData } from "./games/GameServer";
import { getSession } from "./routes/auth.routes";
import { GameWsClientSchema } from "./games/types";
export interface WebServerConfig {
port?: number;
hostname?: string;
}
// Register game plugins
import { gameRegistry } from "@shared/games/registry";
import { chessPlugin } from "@shared/games/chess/chess.plugin";
import { blackjackPlugin } from "@shared/games/blackjack/blackjack.plugin";
gameRegistry.register(chessPlugin);
gameRegistry.register(blackjackPlugin);
export interface WebServerInstance {
server: ReturnType<typeof serve>;
stop: () => Promise<void>;
url: string;
}
const WS_CONFIG = {
MAX_CONNECTIONS: 200,
MAX_PAYLOAD_BYTES: 16384,
IDLE_TIMEOUT_SECONDS: 60,
STATS_BROADCAST_INTERVAL_MS: 5000,
} as const;
/**
* Creates and starts the API server.
*
* @param config - Server configuration options
* @param config.port - Port to listen on (default: 3000)
* @param config.hostname - Hostname to bind to (default: "localhost")
* @returns Promise resolving to server instance with stop() method
*
* @example
* const server = await createWebServer({ port: 3000, hostname: "0.0.0.0" });
* console.log(`Server running at ${server.url}`);
*
* // To stop the server:
* await server.stop();
*/
const MIME_TYPES: Record<string, string> = {
".html": "text/html",
".js": "application/javascript",
@@ -52,6 +45,17 @@ const MIME_TYPES: Record<string, string> = {
".woff2": "font/woff2",
};
export interface WebServerConfig {
port?: number;
hostname?: string;
}
export interface WebServerInstance {
server: ReturnType<typeof serve>;
stop: () => Promise<void>;
url: string;
}
/**
* Serve static files from the panel dist directory.
* Falls back to index.html for SPA routing.
@@ -90,61 +94,63 @@ async function servePanelStatic(pathname: string, distDir: string): Promise<Resp
export async function createWebServer(config: WebServerConfig = {}): Promise<WebServerInstance> {
const { port = 3000, hostname = "localhost" } = config;
// Configuration constants
const MAX_CONNECTIONS = 10;
const MAX_PAYLOAD_BYTES = 16384; // 16KB
const IDLE_TIMEOUT_SECONDS = 60;
// Interval for broadcasting stats to all connected WS clients
let activeConnections = 0;
let statsBroadcastInterval: Timer | undefined;
const server = serve({
const server = serve<WsConnectionData>({
port,
hostname,
async fetch(req, server) {
const url = new URL(req.url);
// WebSocket upgrade handling
if (url.pathname === "/ws") {
const currentConnections = server.pendingWebSockets;
if (currentConnections >= MAX_CONNECTIONS) {
logger.warn("web", `Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`);
if (activeConnections >= WS_CONFIG.MAX_CONNECTIONS) {
logger.warn("web", `Connection rejected: limit reached (${activeConnections}/${WS_CONFIG.MAX_CONNECTIONS})`);
return new Response("Connection limit reached", { status: 429 });
}
const success = server.upgrade(req);
const session = getSession(req);
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
const success = server.upgrade(req, {
data: {
session: {
discordId: session.discordId,
username: session.username,
role: session.role,
},
rooms: new Set<string>(),
},
});
if (success) return undefined;
return new Response("WebSocket upgrade failed", { status: 400 });
}
// Delegate to modular route handlers
const response = await handleRequest(req, url);
if (response) return response;
// Serve panel static files (production)
const panelDistDir = join(import.meta.dir, "../../panel/dist");
const staticResponse = await servePanelStatic(url.pathname, panelDistDir);
if (staticResponse) return staticResponse;
// No matching route found
return new Response("Not Found", { status: 404 });
},
websocket: {
/**
* Called when a WebSocket client connects.
* Subscribes the client to the dashboard channel and sends initial stats.
*/
open(ws) {
open(ws: ServerWebSocket<WsConnectionData>) {
activeConnections++;
ws.subscribe("dashboard");
logger.debug("web", `Client connected. Total: ${server.pendingWebSockets}`);
ws.subscribe("lobby");
logger.debug("web", `Client connected: ${ws.data.session.discordId}. Total: ${activeConnections}`);
// Send initial stats
getFullDashboardStats().then(stats => {
ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats }));
});
// Start broadcast interval if this is the first client
gameServer.handleOpen(ws);
if (!statsBroadcastInterval) {
statsBroadcastInterval = setInterval(async () => {
try {
@@ -153,61 +159,69 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
} catch (error) {
logger.error("web", "Error in stats broadcast", error);
}
}, 5000);
}, WS_CONFIG.STATS_BROADCAST_INTERVAL_MS);
}
},
/**
* Called when a WebSocket message is received.
* Handles PING/PONG heartbeat messages.
*/
async message(ws, message) {
async message(ws: ServerWebSocket<WsConnectionData>, message) {
try {
const messageStr = message.toString();
// Defense-in-depth: redundant length check before parsing
if (messageStr.length > MAX_PAYLOAD_BYTES) {
if (messageStr.length > WS_CONFIG.MAX_PAYLOAD_BYTES) {
logger.error("web", "Payload exceeded maximum limit");
return;
}
const rawData = JSON.parse(messageStr);
const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types");
const parsed = WsMessageSchema.safeParse(rawData);
if (!parsed.success) {
logger.error("web", "Invalid message format", parsed.error.issues);
// Handle dashboard-level messages (PING, etc.)
if (rawData && typeof rawData === "object" && rawData.type === "PING") {
ws.send(JSON.stringify({ type: "PONG" }));
return;
}
if (parsed.data.type === "PING") {
ws.send(JSON.stringify({ type: "PONG" }));
// Route game messages — try to parse as a game client message
const gameCheck = GameWsClientSchema.safeParse(rawData);
if (gameCheck.success) {
gameServer.handleMessage(ws, rawData).catch(err =>
logger.error("web", `Game message handler error: ${err}`),
);
return;
}
// Fall back to full WsMessageSchema for dashboard messages that aren't PING
const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types");
const parsed = WsMessageSchema.safeParse(rawData);
if (!parsed?.success) {
logger.error("web", "Invalid message format", parsed?.error.issues);
}
// Nothing else to do for PONG/STATS_UPDATE/NEW_EVENT from clients
} catch (e) {
logger.error("web", "Failed to handle message", e);
}
},
/**
* Called when a WebSocket client disconnects.
* Stops the broadcast interval if no clients remain.
*/
close(ws) {
close(ws: ServerWebSocket<WsConnectionData>) {
activeConnections--;
ws.unsubscribe("dashboard");
logger.debug("web", `Client disconnected. Total remaining: ${server.pendingWebSockets}`);
ws.unsubscribe("lobby");
logger.debug("web", `Client disconnected: ${ws.data.session.discordId}. Total remaining: ${activeConnections}`);
// Stop broadcast interval if no clients left
if (server.pendingWebSockets === 0 && statsBroadcastInterval) {
gameServer.handleClose(ws);
if (activeConnections === 0 && statsBroadcastInterval) {
clearInterval(statsBroadcastInterval);
statsBroadcastInterval = undefined;
}
},
maxPayloadLength: MAX_PAYLOAD_BYTES,
idleTimeout: IDLE_TIMEOUT_SECONDS,
maxPayloadLength: WS_CONFIG.MAX_PAYLOAD_BYTES,
idleTimeout: WS_CONFIG.IDLE_TIMEOUT_SECONDS,
},
});
// Listen for real-time events from the system bus
// Wire gameServer to Bun server for pub/sub publishing
gameServer.setServer(server);
const { systemEvents, EVENTS } = await import("@shared/lib/events");
systemEvents.on(EVENTS.DASHBOARD.NEW_EVENT, (event) => {
server.publish("dashboard", JSON.stringify({ type: "NEW_EVENT", data: event }));
@@ -226,18 +240,3 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
},
};
}
/**
* Starts the web server from the main application root.
* Kept for backward compatibility.
*
* @param webProjectPath - Deprecated, no longer used
* @param config - Server configuration options
* @returns Promise resolving to server instance
*/
export async function startWebServerFromRoot(
webProjectPath: string,
config: WebServerConfig = {}
): Promise<WebServerInstance> {
return createWebServer(config);
}

View File

@@ -11,6 +11,7 @@ import {
getCancelledEmbed
} from "@/modules/moderation/prune.view";
import { withCommandErrorHandling } from "@lib/commandUtils";
import { PRUNE_CUSTOM_IDS } from "@modules/moderation/prune.types";
export const prune = createCommand({
data: new SlashCommandBuilder()
@@ -83,7 +84,7 @@ export const prune = createCommand({
time: 30000
});
if (confirmation.customId === "cancel_prune") {
if (confirmation.customId === PRUNE_CUSTOM_IDS.CANCEL) {
await confirmation.update({
embeds: [getCancelledEmbed()],
components: []

View File

@@ -2,11 +2,12 @@ import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags } from "discord.js";
import { questService } from "@shared/modules/quest/quest.service";
import { createSuccessEmbed } from "@lib/embeds";
import {
getQuestListComponents,
getAvailableQuestsComponents,
getQuestActionRows
import {
getQuestListComponents,
getAvailableQuestsComponents,
getQuestActionRows
} from "@/modules/quest/quest.view";
import { QUEST_CUSTOM_IDS } from "@modules/quest/quest.types";
export const quests = createCommand({
data: new SlashCommandBuilder()
@@ -56,19 +57,19 @@ export const quests = createCommand({
if (i.user.id !== interaction.user.id) return;
try {
if (i.customId === "quest_view_active") {
if (i.customId === QUEST_CUSTOM_IDS.VIEW_ACTIVE) {
await i.deferUpdate();
await updateView('active', 0);
} else if (i.customId === "quest_view_available") {
} else if (i.customId === QUEST_CUSTOM_IDS.VIEW_AVAILABLE) {
await i.deferUpdate();
await updateView('available', 0);
} else if (i.customId === "quest_page_prev") {
} else if (i.customId === QUEST_CUSTOM_IDS.PAGE_PREV) {
await i.deferUpdate();
await updateView(currentView, Math.max(0, currentPage - 1));
} else if (i.customId === "quest_page_next") {
} else if (i.customId === QUEST_CUSTOM_IDS.PAGE_NEXT) {
await i.deferUpdate();
await updateView(currentView, currentPage + 1);
} else if (i.customId.startsWith("quest_accept:")) {
} else if (i.customId.startsWith(QUEST_CUSTOM_IDS.ACCEPT_PREFIX)) {
const questIdStr = i.customId.split(":")[1];
if (!questIdStr) return;
const questId = parseInt(questIdStr);

View File

@@ -1,10 +1,8 @@
import { AuroraClient } from "@/lib/BotClient";
import { env } from "@shared/lib/env";
import { join } from "node:path";
import { initializeConfig } from "@shared/lib/config";
import { registerDomainEventListeners } from "@shared/lib/eventWiring";
import { startWebServerFromRoot } from "../api/src/server";
import { createWebServer } from "../api/src/server";
// Initialize config from database
await initializeConfig();
@@ -22,12 +20,11 @@ console.log("🌐 Starting web server...");
let shuttingDown = false;
const webProjectPath = join(import.meta.dir, "../api");
const webPort = Number(process.env.WEB_PORT) || 3000;
const webHost = process.env.HOST || "0.0.0.0";
// Start web server in the same process
const webServer = await startWebServerFromRoot(webProjectPath, {
const webServer = await createWebServer({
port: webPort,
hostname: webHost,
});
@@ -54,4 +51,4 @@ const shutdownHandler = async () => {
};
process.on("SIGINT", shutdownHandler);
process.on("SIGTERM", shutdownHandler);
process.on("SIGTERM", shutdownHandler);

View File

@@ -1,11 +1,14 @@
import { ButtonInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from "discord.js";
import { TRADE_CUSTOM_IDS } from "@modules/trade/trade.types";
import { SHOP_CUSTOM_IDS, LOOTDROP_CUSTOM_IDS } from "@modules/economy/economy.types";
import { ITEM_WIZARD_CUSTOM_IDS } from "@modules/admin/item_wizard.types";
import { TRIVIA_CUSTOM_IDS } from "@modules/trivia/trivia.types";
import { ENROLLMENT_CUSTOM_IDS } from "@modules/user/user.types";
import { FEEDBACK_CUSTOM_IDS } from "@modules/feedback/feedback.types";
// Union type for all component interactions
type ComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction;
// Type for the handler function that modules export
type InteractionHandler = (interaction: ComponentInteraction) => Promise<void>;
// Type for the dynamically imported module containing the handler
interface InteractionModule {
[key: string]: (...args: any[]) => Promise<void> | any;
@@ -21,45 +24,45 @@ interface InteractionRoute {
export const interactionRoutes: InteractionRoute[] = [
// --- TRADE MODULE ---
{
predicate: (i) => i.customId.startsWith("trade_") || i.customId === "amount",
predicate: (i) => i.customId.startsWith(TRADE_CUSTOM_IDS.PREFIX) || i.customId === TRADE_CUSTOM_IDS.MONEY_AMOUNT_FIELD,
handler: () => import("@/modules/trade/trade.interaction"),
method: 'handleTradeInteraction'
},
// --- ECONOMY MODULE ---
{
predicate: (i) => i.isButton() && i.customId.startsWith("shop_buy_"),
predicate: (i) => i.isButton() && i.customId.startsWith(SHOP_CUSTOM_IDS.BUY_PREFIX),
handler: () => import("@/modules/economy/shop.interaction"),
method: 'handleShopInteraction'
},
{
predicate: (i) => i.isButton() && i.customId.startsWith("lootdrop_"),
predicate: (i) => i.isButton() && i.customId.startsWith(LOOTDROP_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/economy/lootdrop.interaction"),
method: 'handleLootdropInteraction'
},
{
predicate: (i) => i.isButton() && i.customId.startsWith("trivia_"),
predicate: (i) => i.isButton() && i.customId.startsWith(TRIVIA_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/trivia/trivia.interaction"),
method: 'handleTriviaInteraction'
},
// --- ADMIN MODULE ---
{
predicate: (i) => i.customId.startsWith("createitem_"),
predicate: (i) => i.customId.startsWith(ITEM_WIZARD_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/admin/item_wizard"),
method: 'handleItemWizardInteraction'
},
// --- USER MODULE ---
{
predicate: (i) => i.isButton() && i.customId === "enrollment",
predicate: (i) => i.isButton() && i.customId === ENROLLMENT_CUSTOM_IDS.ENROLL,
handler: () => import("@/modules/user/enrollment.interaction"),
method: 'handleEnrollmentInteraction'
},
// --- FEEDBACK MODULE ---
{
predicate: (i) => i.customId.startsWith("feedback_"),
predicate: (i) => i.customId.startsWith(FEEDBACK_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/feedback/feedback.interaction"),
method: 'handleFeedbackInteraction'
}

View File

@@ -3,7 +3,7 @@ import { items } from "@db/schema";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import type { ItemUsageData, ItemEffect } from "@shared/lib/types";
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
import type { DraftItem } from "./item_wizard.types";
import { ITEM_WIZARD_CUSTOM_IDS, type DraftItem } from "./item_wizard.types";
import { ItemType, EffectType } from "@shared/lib/constants";
// --- Types ---
@@ -41,13 +41,13 @@ export const renderWizard = (userId: string, isDraft = true) => {
export const handleItemWizardInteraction = async (interaction: Interaction) => {
// Only handle createitem interactions
if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return;
if (!interaction.customId.startsWith("createitem_")) return;
if (!interaction.customId.startsWith(ITEM_WIZARD_CUSTOM_IDS.PREFIX)) return;
const userId = interaction.user.id;
let draft = draftSession.get(userId);
// Special case for Cancel - doesn't need draft checks usually, but we want to clear it
if (interaction.customId === "createitem_cancel") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.CANCEL) {
draftSession.delete(userId);
if (interaction.isMessageComponent()) {
await interaction.update({ content: "❌ Item creation cancelled.", embeds: [], components: [] });
@@ -59,7 +59,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
if (!draft) {
if (interaction.isMessageComponent()) {
// Create one implicitly to prevent crashes, or warn user
if (interaction.customId === "createitem_start") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.START) {
// Allow start
} else {
await interaction.reply({ content: "⚠️ Session expired. Please run `/createitem` again.", ephemeral: true });
@@ -81,7 +81,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
// --- Routing ---
// 1. Details Modal
if (interaction.customId === "createitem_details") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.DETAILS) {
if (!interaction.isButton()) return;
const modal = getDetailsModal(draft);
await interaction.showModal(modal);
@@ -89,7 +89,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
}
// 2. Economy Modal
if (interaction.customId === "createitem_economy") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.ECONOMY) {
if (!interaction.isButton()) return;
const modal = getEconomyModal(draft);
await interaction.showModal(modal);
@@ -97,7 +97,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
}
// 3. Visuals Modal
if (interaction.customId === "createitem_visuals") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.VISUALS) {
if (!interaction.isButton()) return;
const modal = getVisualsModal(draft);
await interaction.showModal(modal);
@@ -105,14 +105,14 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
}
// 4. Type Toggle (Start Select Menu)
if (interaction.customId === "createitem_type_toggle") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.TYPE_TOGGLE) {
if (!interaction.isButton()) return;
const { components } = getItemTypeSelection();
await interaction.update({ components }); // Temporary view
return;
}
if (interaction.customId === "createitem_select_type") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.SELECT_TYPE) {
if (!interaction.isStringSelectMenu()) return;
const selected = interaction.values[0];
if (selected) {
@@ -125,14 +125,14 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
}
// 5. Add Effect Flow
if (interaction.customId === "createitem_addeffect_start") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.ADD_EFFECT_START) {
if (!interaction.isButton()) return;
const { components } = getEffectTypeSelection();
await interaction.update({ components });
return;
}
if (interaction.customId === "createitem_select_effect_type") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.SELECT_EFFECT_TYPE) {
if (!interaction.isStringSelectMenu()) return;
const effectType = interaction.values[0];
if (!effectType) return;
@@ -149,7 +149,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
}
// Toggle Consume
if (interaction.customId === "createitem_toggle_consume") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.TOGGLE_CONSUME) {
if (!interaction.isButton()) return;
draft.usageData.consume = !draft.usageData.consume;
const payload = renderWizard(userId);
@@ -159,43 +159,43 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
// 6. Handle Modal Submits
if (interaction.isModalSubmit()) {
if (interaction.customId === "createitem_modal_details") {
draft.name = interaction.fields.getTextInputValue("name");
draft.description = interaction.fields.getTextInputValue("desc");
draft.rarity = interaction.fields.getTextInputValue("rarity");
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_DETAILS) {
draft.name = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_NAME);
draft.description = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_DESC);
draft.rarity = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_RARITY);
}
else if (interaction.customId === "createitem_modal_economy") {
const price = parseInt(interaction.fields.getTextInputValue("price"));
else if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_ECONOMY) {
const price = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_PRICE));
draft.price = isNaN(price) || price === 0 ? null : price;
}
else if (interaction.customId === "createitem_modal_visuals") {
draft.iconUrl = interaction.fields.getTextInputValue("icon");
draft.imageUrl = interaction.fields.getTextInputValue("image");
else if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_VISUALS) {
draft.iconUrl = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_ICON);
draft.imageUrl = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_IMAGE);
}
else if (interaction.customId === "createitem_modal_effect") {
else if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_EFFECT) {
const type = draft.pendingEffectType;
if (type) {
let effect: ItemEffect | null = null;
if (type === EffectType.ADD_XP || type === EffectType.ADD_BALANCE) {
const amount = parseInt(interaction.fields.getTextInputValue("amount"));
const amount = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_AMOUNT));
if (!isNaN(amount)) effect = { type: type as any, amount };
}
else if (type === EffectType.REPLY_MESSAGE) {
effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue("message") };
effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_MESSAGE) };
}
else if (type === EffectType.XP_BOOST) {
const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier"));
const duration = parseInt(interaction.fields.getTextInputValue("duration"));
const multiplier = parseFloat(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_MULTIPLIER));
const duration = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION));
if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: EffectType.XP_BOOST, multiplier, durationSeconds: duration };
}
else if (type === EffectType.TEMP_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id");
const duration = parseInt(interaction.fields.getTextInputValue("duration"));
const roleId = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID);
const duration = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION));
if (roleId && !isNaN(duration)) effect = { type: EffectType.TEMP_ROLE, roleId: roleId, durationSeconds: duration };
}
else if (type === EffectType.COLOR_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id");
const roleId = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID);
if (roleId) effect = { type: EffectType.COLOR_ROLE, roleId: roleId };
}
@@ -214,7 +214,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
}
// 7. Save
if (interaction.customId === "createitem_save") {
if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.SAVE) {
if (!interaction.isButton()) return;
await interaction.deferUpdate(); // Prepare to save

View File

@@ -1,5 +1,36 @@
import type { ItemUsageData } from "@shared/lib/types";
export const ITEM_WIZARD_CUSTOM_IDS = {
PREFIX: "createitem_",
START: "createitem_start",
DETAILS: "createitem_details",
ECONOMY: "createitem_economy",
VISUALS: "createitem_visuals",
TYPE_TOGGLE: "createitem_type_toggle",
SELECT_TYPE: "createitem_select_type",
ADD_EFFECT_START: "createitem_addeffect_start",
SELECT_EFFECT_TYPE: "createitem_select_effect_type",
TOGGLE_CONSUME: "createitem_toggle_consume",
SAVE: "createitem_save",
CANCEL: "createitem_cancel",
MODAL_DETAILS: "createitem_modal_details",
MODAL_ECONOMY: "createitem_modal_economy",
MODAL_VISUALS: "createitem_modal_visuals",
MODAL_EFFECT: "createitem_modal_effect",
// Modal field IDs
FIELD_NAME: "name",
FIELD_DESC: "desc",
FIELD_RARITY: "rarity",
FIELD_PRICE: "price",
FIELD_ICON: "icon",
FIELD_IMAGE: "image",
FIELD_AMOUNT: "amount",
FIELD_MESSAGE: "message",
FIELD_MULTIPLIER: "multiplier",
FIELD_DURATION: "duration",
FIELD_ROLE_ID: "role_id",
} as const;
export interface DraftItem {
name: string;
description: string;

View File

@@ -9,7 +9,7 @@ import {
type MessageActionRowComponentBuilder
} from "discord.js";
import { createBaseEmbed } from "@lib/embeds";
import type { DraftItem } from "./item_wizard.types";
import { ITEM_WIZARD_CUSTOM_IDS, type DraftItem } from "./item_wizard.types";
import { ItemType } from "@shared/lib/constants";
const getItemTypeOptions = () => [
@@ -51,18 +51,18 @@ export const getItemWizardEmbed = (draft: DraftItem) => {
// Components
const row1 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents(
new ButtonBuilder().setCustomId("createitem_details").setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"),
new ButtonBuilder().setCustomId("createitem_economy").setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"),
new ButtonBuilder().setCustomId("createitem_visuals").setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"),
new ButtonBuilder().setCustomId("createitem_type_toggle").setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.DETAILS).setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.ECONOMY).setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.VISUALS).setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.TYPE_TOGGLE).setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
);
const row2 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents(
new ButtonBuilder().setCustomId("createitem_addeffect_start").setLabel("Add Effect").setStyle(ButtonStyle.Primary).setEmoji("✨"),
new ButtonBuilder().setCustomId("createitem_toggle_consume").setLabel(`Consume: ${draft.usageData.consume ? "ON" : "OFF"}`).setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
new ButtonBuilder().setCustomId("createitem_save").setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"),
new ButtonBuilder().setCustomId("createitem_cancel").setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️")
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.ADD_EFFECT_START).setLabel("Add Effect").setStyle(ButtonStyle.Primary).setEmoji("✨"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.TOGGLE_CONSUME).setLabel(`Consume: ${draft.usageData.consume ? "ON" : "OFF"}`).setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.SAVE).setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"),
new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.CANCEL).setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️")
);
return { embeds: [embed], components: [row1, row2] };
@@ -70,65 +70,65 @@ export const getItemWizardEmbed = (draft: DraftItem) => {
export const getItemTypeSelection = () => {
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder().setCustomId("createitem_select_type").setPlaceholder("Select Item Type").addOptions(getItemTypeOptions())
new StringSelectMenuBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.SELECT_TYPE).setPlaceholder("Select Item Type").addOptions(getItemTypeOptions())
);
return { components: [row] };
};
export const getEffectTypeSelection = () => {
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder().setCustomId("createitem_select_effect_type").setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions())
new StringSelectMenuBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.SELECT_EFFECT_TYPE).setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions())
);
return { components: [row] };
};
export const getDetailsModal = (current: DraftItem) => {
const modal = new ModalBuilder().setCustomId("createitem_modal_details").setTitle("Edit Details");
const modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_DETAILS).setTitle("Edit Details");
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("name").setLabel("Name").setValue(current.name).setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("desc").setLabel("Description").setValue(current.description).setStyle(TextInputStyle.Paragraph).setRequired(false)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("rarity").setLabel("Rarity").setValue(current.rarity).setStyle(TextInputStyle.Short).setPlaceholder("C, R, SR, SSR").setRequired(true))
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_NAME).setLabel("Name").setValue(current.name).setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_DESC).setLabel("Description").setValue(current.description).setStyle(TextInputStyle.Paragraph).setRequired(false)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_RARITY).setLabel("Rarity").setValue(current.rarity).setStyle(TextInputStyle.Short).setPlaceholder("C, R, SR, SSR").setRequired(true))
);
return modal;
};
export const getEconomyModal = (current: DraftItem) => {
const modal = new ModalBuilder().setCustomId("createitem_modal_economy").setTitle("Edit Economy");
const modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_ECONOMY).setTitle("Edit Economy");
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("price").setLabel("Price (0 for not for sale)").setValue(current.price?.toString() || "0").setStyle(TextInputStyle.Short).setRequired(true))
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_PRICE).setLabel("Price (0 for not for sale)").setValue(current.price?.toString() || "0").setStyle(TextInputStyle.Short).setRequired(true))
);
return modal;
};
export const getVisualsModal = (current: DraftItem) => {
const modal = new ModalBuilder().setCustomId("createitem_modal_visuals").setTitle("Edit Visuals");
const modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_VISUALS).setTitle("Edit Visuals");
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("icon").setLabel("Icon URL (Emoji or Link)").setValue(current.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("image").setLabel("Image URL").setValue(current.imageUrl).setStyle(TextInputStyle.Short).setRequired(false))
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_ICON).setLabel("Icon URL (Emoji or Link)").setValue(current.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_IMAGE).setLabel("Image URL").setValue(current.imageUrl).setStyle(TextInputStyle.Short).setRequired(false))
);
return modal;
};
export const getEffectConfigModal = (effectType: string) => {
let modal = new ModalBuilder().setCustomId("createitem_modal_effect").setTitle(`Config ${effectType}`);
let modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_EFFECT).setTitle(`Config ${effectType}`);
if (effectType === "ADD_XP" || effectType === "ADD_BALANCE") {
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short).setRequired(true).setPlaceholder("100")));
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_AMOUNT).setLabel("Amount").setStyle(TextInputStyle.Short).setRequired(true).setPlaceholder("100")));
} else if (effectType === "REPLY_MESSAGE") {
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("message").setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true)));
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_MESSAGE).setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true)));
} else if (effectType === "XP_BOOST") {
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("multiplier").setLabel("Multiplier (e.g. 1.5)").setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_MULTIPLIER).setLabel("Multiplier (e.g. 1.5)").setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION).setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
);
} else if (effectType === "TEMP_ROLE") {
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID).setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION).setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
);
} else if (effectType === "COLOR_ROLE") {
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true))
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID).setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true))
);
}
return modal;

View File

@@ -0,0 +1,10 @@
export const LOOTDROP_CUSTOM_IDS = {
PREFIX: "lootdrop_",
CLAIM: "lootdrop_claim",
CLAIM_DISABLED: "lootdrop_claim_disabled",
} as const;
export const SHOP_CUSTOM_IDS = {
BUY_PREFIX: "shop_buy_",
BUY: (itemId: number) => `shop_buy_${itemId}`,
} as const;

View File

@@ -3,9 +3,10 @@ import { lootdropService } from "@shared/modules/economy/lootdrop.service";
import { UserError } from "@shared/lib/errors";
import { getLootdropClaimedMessage } from "./lootdrop.view";
import { terminalService } from "@modules/system/terminal.service";
import { LOOTDROP_CUSTOM_IDS } from "./economy.types";
export async function handleLootdropInteraction(interaction: ButtonInteraction) {
if (interaction.customId === "lootdrop_claim") {
if (interaction.customId === LOOTDROP_CUSTOM_IDS.CLAIM) {
await interaction.deferReply({ ephemeral: true });
const result = await lootdropService.tryClaim(interaction.message.id, interaction.user.id, interaction.user.username);

View File

@@ -1,12 +1,13 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import { generateLootdropCard, generateClaimedLootdropCard } from "@/graphics/lootdrop";
import { LOOTDROP_CUSTOM_IDS } from "./economy.types";
export async function getLootdropMessage(reward: number, currency: string) {
const cardBuffer = await generateLootdropCard(reward, currency);
const attachment = new AttachmentBuilder(cardBuffer, { name: "lootdrop.png" });
const claimButton = new ButtonBuilder()
.setCustomId("lootdrop_claim")
.setCustomId(LOOTDROP_CUSTOM_IDS.CLAIM)
.setLabel("CLAIM REWARD")
.setStyle(ButtonStyle.Secondary) // Changed to Secondary to fit the darker theme better? Or keep Success? Let's try Secondary with custom emoji
.setEmoji("🌠");
@@ -28,7 +29,7 @@ export async function getLootdropClaimedMessage(userId: string, username: string
const newRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId("lootdrop_claim_disabled")
.setCustomId(LOOTDROP_CUSTOM_IDS.CLAIM_DISABLED)
.setLabel("CLAIMED")
.setStyle(ButtonStyle.Secondary)
.setEmoji("✅")

View File

@@ -2,13 +2,14 @@ import { ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { UserError } from "@shared/lib/errors";
import { SHOP_CUSTOM_IDS } from "./economy.types";
export async function handleShopInteraction(interaction: ButtonInteraction) {
if (!interaction.customId.startsWith("shop_buy_")) return;
if (!interaction.customId.startsWith(SHOP_CUSTOM_IDS.BUY_PREFIX)) return;
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const itemId = parseInt(interaction.customId.replace("shop_buy_", ""));
const itemId = parseInt(interaction.customId.replace(SHOP_CUSTOM_IDS.BUY_PREFIX, ""));
if (isNaN(itemId)) {
throw new UserError("Invalid Item ID.");
}

View File

@@ -19,6 +19,7 @@ import { existsSync } from "fs";
import { LootType, EffectType } from "@shared/lib/constants";
import type { LootTableItem } from "@shared/lib/types";
import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity";
import { SHOP_CUSTOM_IDS } from "./economy.types";
export function getShopListingMessage(
item: {
@@ -100,7 +101,7 @@ export function getShopListingMessage(
// Create buy button (used in either main or loot container)
const buyButton = new ButtonBuilder()
.setCustomId(`shop_buy_${item.id}`)
.setCustomId(SHOP_CUSTOM_IDS.BUY(item.id))
.setLabel(`Purchase for ${item.price} 🪙`)
.setStyle(ButtonStyle.Success)
.setEmoji("🛒");
@@ -162,8 +163,9 @@ export function getShopListingMessage(
if (line) {
if (!tiers[rarity]) tiers[rarity] = { items: [], totalChance: 0 };
tiers[rarity].items.push(line);
tiers[rarity].totalChance += chance;
const tier = tiers[rarity]!;
tier.items.push(line);
tier.totalChance += chance;
}
}

View File

@@ -8,7 +8,7 @@ import { UserError } from "@shared/lib/errors";
export const handleFeedbackInteraction = async (interaction: Interaction) => {
// Handle select menu for choosing feedback type
if (interaction.isStringSelectMenu() && interaction.customId === "feedback_select_type") {
if (interaction.isStringSelectMenu() && interaction.customId === FEEDBACK_CUSTOM_IDS.SELECT_TYPE) {
const feedbackType = interaction.values[0] as FeedbackType;
if (!feedbackType) {

View File

@@ -16,8 +16,10 @@ export const FEEDBACK_TYPE_LABELS: Record<FeedbackType, string> = {
};
export const FEEDBACK_CUSTOM_IDS = {
PREFIX: "feedback_",
SELECT_TYPE: "feedback_select_type",
MODAL: "feedback_modal",
TYPE_FIELD: "feedback_type",
TITLE_FIELD: "feedback_title",
DESCRIPTION_FIELD: "feedback_description"
DESCRIPTION_FIELD: "feedback_description",
} as const;

View File

@@ -14,7 +14,7 @@ import { FEEDBACK_TYPE_LABELS, FEEDBACK_CUSTOM_IDS, type FeedbackData, type Feed
export function getFeedbackTypeMenu() {
const select = new StringSelectMenuBuilder()
.setCustomId("feedback_select_type")
.setCustomId(FEEDBACK_CUSTOM_IDS.SELECT_TYPE)
.setPlaceholder("Choose feedback type")
.addOptions([
{

View File

@@ -3,6 +3,7 @@ import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { getLootboxResultMessage } from "./inventory.view";
import type { ItemUsageData } from "@shared/lib/types";
import { getGuildConfig } from "@shared/lib/config";
import { INVENTORY_CUSTOM_IDS } from "./inventory.types";
export interface InventoryState {
ownerId: string;
@@ -25,7 +26,7 @@ export function parseInventoryCustomId(customId: string): { action: string; view
* Checks if a custom ID belongs to the inventory system.
*/
export function isInventoryInteraction(customId: string): boolean {
return customId.startsWith("inv_");
return customId.startsWith(INVENTORY_CUSTOM_IDS.PREFIX);
}
/**

View File

@@ -0,0 +1,13 @@
export const INVENTORY_CUSTOM_IDS = {
PREFIX: "inv_",
SELECT: (viewerId: string) => `inv_select_${viewerId}`,
PREV: (viewerId: string) => `inv_prev_${viewerId}`,
PAGE: (viewerId: string) => `inv_page_${viewerId}`,
NEXT: (viewerId: string) => `inv_next_${viewerId}`,
BACK: (viewerId: string) => `inv_back_${viewerId}`,
USE: (viewerId: string) => `inv_use_${viewerId}`,
DISCARD: (viewerId: string) => `inv_discard_${viewerId}`,
DISCARD_CONFIRM: (viewerId: string) => `inv_discard_confirm_${viewerId}`,
DISCARD_CANCEL: (viewerId: string) => `inv_discard_cancel_${viewerId}`,
USE_BACK: (viewerId: string) => `inv_use_back_${viewerId}`,
} as const;

View File

@@ -22,6 +22,7 @@ import { ItemType } from "@shared/lib/constants";
import type { ItemUsageData } from "@shared/lib/types";
import { join } from "path";
import { existsSync } from "fs";
import { INVENTORY_CUSTOM_IDS } from "./inventory.types";
export const ITEMS_PER_PAGE = 5;
@@ -101,7 +102,7 @@ export function getInventoryListMessage(
// Select menu with current page items
const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`inv_select_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.SELECT(viewerId))
.setPlaceholder("Select an item for details");
for (const entry of pageItems) {
@@ -121,17 +122,17 @@ export function getInventoryListMessage(
// Pagination buttons
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_prev_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.PREV(viewerId))
.setLabel("◀ Previous")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage <= 0),
new ButtonBuilder()
.setCustomId(`inv_page_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.PAGE(viewerId))
.setLabel(`Page ${safePage + 1}/${totalPages}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId(`inv_next_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.NEXT(viewerId))
.setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage >= totalPages - 1),
@@ -210,7 +211,7 @@ export function getItemDetailMessage(
);
// Stats row
const priceText = item.price ? `${item.price} 🪙` : "Not tradeable";
const priceText = item.price ? `${item.price} 🪙` : "Not purchasable";
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Owned: **×${entry.quantity}** · Value: **${priceText}**`
@@ -225,7 +226,7 @@ export function getItemDetailMessage(
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_back_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.BACK(viewerId))
.setLabel("◀ Back")
.setStyle(ButtonStyle.Primary)
);
@@ -233,7 +234,7 @@ export function getItemDetailMessage(
if (isUsable) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_use_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.USE(viewerId))
.setLabel("🧪 Use")
.setStyle(ButtonStyle.Success)
);
@@ -242,7 +243,7 @@ export function getItemDetailMessage(
if (isOwner) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.DISCARD(viewerId))
.setLabel("🗑 Discard")
.setStyle(ButtonStyle.Danger)
);
@@ -271,11 +272,11 @@ export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string
.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_confirm_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.DISCARD_CONFIRM(viewerId))
.setLabel("Confirm")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`inv_discard_cancel_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.DISCARD_CANCEL(viewerId))
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary)
)
@@ -296,7 +297,7 @@ export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string
export function appendUseBackButton(message: any, viewerId: string): any {
const backRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_use_back_${viewerId}`)
.setCustomId(INVENTORY_CUSTOM_IDS.USE_BACK(viewerId))
.setLabel("◀ Back to Inventory")
.setStyle(ButtonStyle.Primary)
);

View File

@@ -1,3 +1,8 @@
export const PRUNE_CUSTOM_IDS = {
CONFIRM: "confirm_prune",
CANCEL: "cancel_prune",
} as const;
export interface PruneOptions {
amount?: number;
userId?: string;

View File

@@ -1,5 +1,5 @@
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, Colors } from "discord.js";
import type { PruneResult, PruneProgress } from "./prune.types";
import { PRUNE_CUSTOM_IDS, type PruneResult, type PruneProgress } from "./prune.types";
/**
* Creates a confirmation message for prune operations
@@ -25,12 +25,12 @@ export function getConfirmationMessage(
.setTimestamp();
const confirmButton = new ButtonBuilder()
.setCustomId("confirm_prune")
.setCustomId(PRUNE_CUSTOM_IDS.CONFIRM)
.setLabel("Confirm")
.setStyle(ButtonStyle.Danger);
const cancelButton = new ButtonBuilder()
.setCustomId("cancel_prune")
.setCustomId(PRUNE_CUSTOM_IDS.CANCEL)
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary);

View File

@@ -0,0 +1,8 @@
export const QUEST_CUSTOM_IDS = {
ACCEPT_PREFIX: "quest_accept:",
ACCEPT: (questId: number) => `quest_accept:${questId}`,
PAGE_PREV: "quest_page_prev",
PAGE_NEXT: "quest_page_next",
VIEW_ACTIVE: "quest_view_active",
VIEW_AVAILABLE: "quest_view_available",
} as const;

View File

@@ -8,6 +8,7 @@ import {
SeparatorSpacingSize,
MessageFlags
} from "discord.js";
import { QUEST_CUSTOM_IDS } from "./quest.types";
/**
* Quest entry with quest details and progress
@@ -169,7 +170,7 @@ export function getAvailableQuestsComponents(availableQuests: AvailableQuest[],
container.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`quest_accept:${quest.id}`)
.setCustomId(QUEST_CUSTOM_IDS.ACCEPT(quest.id))
.setLabel("Accept Quest")
.setStyle(ButtonStyle.Success)
.setEmoji("✅")
@@ -191,12 +192,12 @@ export function getQuestActionRows(viewType: 'active' | 'available', totalItems:
if (totalPages > 1) {
rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("quest_page_prev")
.setCustomId(QUEST_CUSTOM_IDS.PAGE_PREV)
.setLabel("◀ Prev")
.setStyle(ButtonStyle.Secondary)
.setDisabled(page <= 0),
new ButtonBuilder()
.setCustomId("quest_page_next")
.setCustomId(QUEST_CUSTOM_IDS.PAGE_NEXT)
.setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary)
.setDisabled(page >= totalPages - 1)
@@ -206,12 +207,12 @@ export function getQuestActionRows(viewType: 'active' | 'available', totalItems:
// Tab navigation row
rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("quest_view_active")
.setCustomId(QUEST_CUSTOM_IDS.VIEW_ACTIVE)
.setLabel("📜 Active")
.setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setDisabled(viewType === 'active'),
new ButtonBuilder()
.setCustomId("quest_view_available")
.setCustomId(QUEST_CUSTOM_IDS.VIEW_AVAILABLE)
.setLabel("🗺️ Available")
.setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setDisabled(viewType === 'available')

View File

@@ -12,6 +12,7 @@ import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds";
import { UserError } from "@shared/lib/errors";
import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view";
import { TRADE_CUSTOM_IDS } from "./trade.types";
@@ -23,25 +24,25 @@ export async function handleTradeInteraction(interaction: Interaction) {
if (!threadId) return;
if (customId === 'trade_cancel') {
if (customId === TRADE_CUSTOM_IDS.CANCEL) {
await handleCancel(interaction, threadId);
} else if (customId === 'trade_lock') {
} else if (customId === TRADE_CUSTOM_IDS.LOCK) {
await handleLock(interaction, threadId);
} else if (customId === 'trade_confirm') {
} else if (customId === TRADE_CUSTOM_IDS.CONFIRM) {
// Confirm logic is handled implicitly by both locking or explicitly if needed.
// For now, locking both triggers execution, so no separate confirm handler is actively used
// unless we re-introduce a specific button. keeping basic handler stub if needed.
} else if (customId === 'trade_add_money') {
} else if (customId === TRADE_CUSTOM_IDS.ADD_MONEY) {
await handleAddMoneyClick(interaction);
} else if (customId === 'trade_money_modal') {
} else if (customId === TRADE_CUSTOM_IDS.MONEY_MODAL) {
await handleMoneySubmit(interaction as ModalSubmitInteraction, threadId);
} else if (customId === 'trade_add_item') {
} else if (customId === TRADE_CUSTOM_IDS.ADD_ITEM) {
await handleAddItemClick(interaction as ButtonInteraction, threadId);
} else if (customId === 'trade_select_item') {
} else if (customId === TRADE_CUSTOM_IDS.SELECT_ITEM) {
await handleItemSelect(interaction as StringSelectMenuInteraction, threadId);
} else if (customId === 'trade_remove_item') {
} else if (customId === TRADE_CUSTOM_IDS.REMOVE_ITEM) {
await handleRemoveItemClick(interaction as ButtonInteraction, threadId);
} else if (customId === 'trade_remove_item_select') {
} else if (customId === TRADE_CUSTOM_IDS.REMOVE_ITEM_SELECT) {
await handleRemoveItemSelect(interaction as StringSelectMenuInteraction, threadId);
}
}
@@ -82,7 +83,7 @@ async function handleAddMoneyClick(interaction: Interaction) {
}
async function handleMoneySubmit(interaction: ModalSubmitInteraction, threadId: string) {
const amountStr = interaction.fields.getTextInputValue('amount');
const amountStr = interaction.fields.getTextInputValue(TRADE_CUSTOM_IDS.MONEY_AMOUNT_FIELD);
const amount = BigInt(amountStr);
if (amount < 0n) throw new UserError("Amount must be positive");
@@ -107,7 +108,7 @@ async function handleAddItemClick(interaction: ButtonInteraction, threadId: stri
description: `Rarity: ${entry.item.rarity} `
}));
const { components } = getItemSelectMenu(options, 'trade_select_item', 'Select an item to add');
const { components } = getItemSelectMenu(options, TRADE_CUSTOM_IDS.SELECT_ITEM, 'Select an item to add');
await interaction.reply({ content: "Select an item to add:", components, ephemeral: true });
}
@@ -142,7 +143,7 @@ async function handleRemoveItemClick(interaction: ButtonInteraction, threadId: s
value: i.id.toString(),
}));
const { components } = getItemSelectMenu(options, 'trade_remove_item_select', 'Select an item to remove');
const { components } = getItemSelectMenu(options, TRADE_CUSTOM_IDS.REMOVE_ITEM_SELECT, 'Select an item to remove');
await interaction.reply({ content: "Select an item to remove:", components, ephemeral: true });
}

View File

@@ -1,3 +1,16 @@
export const TRADE_CUSTOM_IDS = {
PREFIX: "trade_",
ADD_ITEM: "trade_add_item",
ADD_MONEY: "trade_add_money",
REMOVE_ITEM: "trade_remove_item",
LOCK: "trade_lock",
CANCEL: "trade_cancel",
CONFIRM: "trade_confirm",
MONEY_MODAL: "trade_money_modal",
MONEY_AMOUNT_FIELD: "amount",
SELECT_ITEM: "trade_select_item",
REMOVE_ITEM_SELECT: "trade_remove_item_select",
} as const;
export interface TradeItem {
id: number;

View File

@@ -1,6 +1,6 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
import { createBaseEmbed } from "@lib/embeds";
import type { TradeSession, TradeParticipant } from "./trade.types";
import { TRADE_CUSTOM_IDS, type TradeSession, type TradeParticipant } from "./trade.types";
const EMBED_COLOR = 0xFFD700; // Gold
@@ -34,11 +34,11 @@ export function getTradeDashboard(session: TradeSession) {
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder().setCustomId('trade_add_item').setLabel('Add Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('trade_add_money').setLabel('Add Money').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId('trade_remove_item').setLabel('Remove Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('trade_lock').setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId('trade_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger),
new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.ADD_ITEM).setLabel('Add Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.ADD_MONEY).setLabel('Add Money').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.REMOVE_ITEM).setLabel('Remove Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.LOCK).setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.CANCEL).setLabel('Cancel').setStyle(ButtonStyle.Danger),
);
return { embeds: [embed], components: [row] };
@@ -57,11 +57,11 @@ export function getTradeCompletedEmbed(session: TradeSession) {
export function getTradeMoneyModal() {
const modal = new ModalBuilder()
.setCustomId('trade_money_modal')
.setCustomId(TRADE_CUSTOM_IDS.MONEY_MODAL)
.setTitle('Add Money');
const input = new TextInputBuilder()
.setCustomId('amount')
.setCustomId(TRADE_CUSTOM_IDS.MONEY_AMOUNT_FIELD)
.setLabel("Amount to trade")
.setStyle(TextInputStyle.Short)
.setPlaceholder("100")

View File

@@ -1,116 +1,38 @@
# Trivia - Components v2 Implementation
# Trivia UI
This trivia feature uses **Discord Components v2** for a premium visual experience.
The trivia command uses Discord Components v2 for the question, result, and timeout states.
## 🎨 Visual Features
## Files
### **Container with Accent Colors**
Each trivia question is displayed in a Container with a colored accent bar that changes based on difficulty:
- **🟢 Easy**: Green accent bar (`0x57F287`)
- **🟡 Medium**: Yellow accent bar (`0xFEE75C`)
- **🔴 Hard**: Red accent bar (`0xED4245`)
### **Modern Layout Components**
- **TextDisplay** - Rich markdown formatting for question text
- **Separator** - Visual spacing between sections
- **Container** - Groups all content with difficulty-based styling
### **Interactive Features**
**Give Up Button** - Players can forfeit if they're unsure
**Disabled Answer Buttons** - After answering, buttons show:
- ✅ Green for correct answer
- ❌ Red for user's incorrect answer
- Gray for other options
**Time Display** - Shows both relative time (`in 30s`) and seconds remaining
**Stakes Preview** - Clear display: `50 AU ➜ 100 AU`
## 📁 File Structure
```
```text
bot/modules/trivia/
├── trivia.view.ts # Components v2 view functions
├── trivia.interaction.ts # Button interaction handler
└── README.md # This file
trivia.types.ts
trivia.view.ts
trivia.interaction.ts
bot/commands/economy/
└── trivia.ts # /trivia slash command
bot/commands/economy/trivia.ts
shared/modules/trivia/trivia.service.ts
```
## 🔧 Technical Details
## What the view layer does
### Components v2 Requirements
- Uses `MessageFlags.IsComponentsV2` flag
- No `embeds` or `content` fields (uses TextDisplay instead)
- Numeric component types:
- `1` - Action Row
- `2` - Button
- `10` - Text Display
- `14` - Separator
- `17` - Container
- Max 40 components per message (vs 5 for legacy)
- renders the active question as a Components v2 container
- colors the container by difficulty
- renders answer buttons from the session's shuffled answers
- renders separate result and timeout views with disabled buttons
### Button Styles
- **Secondary (2)**: Gray - Used for answer buttons
- **Success (3)**: Green - Used for "True" and correct answers
- **Danger (4)**: Red - Used for "False", incorrect answers, and "Give Up"
## Current interaction flow
## 🎮 User Experience Flow
1. `/trivia` checks cooldown before deferring.
2. `startTrivia()` deducts the entry fee and creates the session.
3. `getTriviaQuestionView()` renders the prompt.
4. `trivia.interaction.ts` compares the clicked answer with the session's `correctIndex`.
5. `submitAnswer()` finalizes the session and the view swaps to success, failure, or timeout output.
1. User runs `/trivia`
2. Sees question in a Container with difficulty-based accent color
3. Can choose to:
- Select an answer (A/B/C/D or True/False)
- Give up using the 🏳️ button
4. After answering, sees result with:
- Disabled buttons showing correct/incorrect answers
- Container with result-based accent color (green/red/yellow)
- Reward or penalty information
The command also schedules a timeout cleanup with a 5-second grace period after `config.trivia.timeoutSeconds`.
## 🌟 Visual Examples
## Custom IDs
### Question Display
```
┌─[GREEN]─────────────────────────┐
│ # 🎯 Trivia Challenge │
│ 🟢 Easy • 📚 Geography │
│ ─────────────────────────── │
│ ### What is the capital of │
│ France? │
│ │
│ ⏱️ Time: in 30s (30s) │
│ 💰 Stakes: 50 AU ➜ 100 AU │
│ 👤 Player: Username │
└─────────────────────────────────┘
[🇦 A: Paris] [🇧 B: London]
[🇨 C: Berlin] [🇩 D: Madrid]
[🏳️ Give Up]
```
### Result Display (Correct)
```
┌─[GREEN]─────────────────────────┐
│ # 🎉 Correct Answer! │
│ ### What is the capital of │
│ France? │
│ ─────────────────────────── │
│ ✅ Your answer: Paris │
│ │
│ 💰 Reward: +100 AU │
│ │
│ 🏆 Great job! Keep it up! │
└─────────────────────────────────┘
[✅ A: Paris] [❌ B: London]
[❌ C: Berlin] [❌ D: Madrid]
(all buttons disabled)
```
## 🚀 Future Enhancements
Potential improvements:
- [ ] Thumbnail images based on trivia category
- [ ] Progress bar for time remaining
- [ ] Streak counter display
- [ ] Category-specific accent colors
- [ ] Media Gallery for image-based questions
- [ ] Leaderboard integration in results
- answer buttons: `TRIVIA_CUSTOM_IDS.ANSWER(sessionId, index)`
- give up: `TRIVIA_CUSTOM_IDS.GIVE_UP(sessionId)`
- result/timeout buttons use non-interactive result IDs

View File

@@ -0,0 +1,7 @@
export const TRIVIA_CUSTOM_IDS = {
PREFIX: "trivia_",
ANSWER: (sessionId: string, index: number) => `trivia_answer_${sessionId}_${index}`,
GIVE_UP: (sessionId: string) => `trivia_giveup_${sessionId}`,
RESULT: (index: number) => `trivia_result_${index}`,
TIMEOUT: (index: number) => `trivia_timeout_${index}`,
} as const;

View File

@@ -1,5 +1,6 @@
import { MessageFlags } from "discord.js";
import type { TriviaSession, TriviaResult } from "@shared/modules/trivia/trivia.service";
import { TRIVIA_CUSTOM_IDS } from "./trivia.types";
/**
* Get color based on difficulty level
@@ -97,14 +98,14 @@ export function getTriviaQuestionView(session: TriviaSession, username: string):
components: [
{
type: 2, // Button
custom_id: `trivia_answer_${sessionId}_${trueIndex}`,
custom_id: TRIVIA_CUSTOM_IDS.ANSWER(sessionId, trueIndex),
label: 'True',
style: 3, // Success
emoji: { name: '✅' }
},
{
type: 2, // Button
custom_id: `trivia_answer_${sessionId}_${falseIndex}`,
custom_id: TRIVIA_CUSTOM_IDS.ANSWER(sessionId, falseIndex),
label: 'False',
style: 4, // Danger
emoji: { name: '❌' }
@@ -129,7 +130,7 @@ export function getTriviaQuestionView(session: TriviaSession, username: string):
buttonRow.components.push({
type: 2, // Button
custom_id: `trivia_answer_${sessionId}_${i}`,
custom_id: TRIVIA_CUSTOM_IDS.ANSWER(sessionId, i),
label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`,
style: 2, // Secondary
emoji: { name: emoji }
@@ -145,7 +146,7 @@ export function getTriviaQuestionView(session: TriviaSession, username: string):
components: [
{
type: 2, // Button
custom_id: `trivia_giveup_${sessionId}`,
custom_id: TRIVIA_CUSTOM_IDS.GIVE_UP(sessionId),
label: 'Give Up',
style: 4, // Danger
emoji: { name: '🏳️' }
@@ -245,7 +246,7 @@ export function getTriviaResultView(
buttonRow.components.push({
type: 2, // Button
custom_id: `trivia_result_${i}`,
custom_id: TRIVIA_CUSTOM_IDS.RESULT(i),
label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`,
style: isCorrect ? 3 : wasUserAnswer ? 4 : 2, // Success : Danger : Secondary
emoji: { name: isCorrect ? '✅' : wasUserAnswer ? '❌' : emoji },
@@ -318,7 +319,7 @@ export function getTriviaTimeoutView(
buttonRow.components.push({
type: 2, // Button
custom_id: `trivia_timeout_${i}`,
custom_id: TRIVIA_CUSTOM_IDS.TIMEOUT(i),
label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`,
style: isCorrect ? 3 : 2, // Success : Secondary
emoji: { name: isCorrect ? '✅' : emoji },

View File

@@ -0,0 +1,3 @@
export const ENROLLMENT_CUSTOM_IDS = {
ENROLL: "enrollment",
} as const;

View File

@@ -6,9 +6,11 @@
"name": "app",
"dependencies": {
"@napi-rs/canvas": "^0.1.89",
"chess.js": "^1.4.0",
"discord.js": "^14.25.1",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"mitt": "^3.0.1",
"postgres": "^3.4.8",
"zod": "^4.3.6",
},
@@ -25,11 +27,14 @@
"version": "0.1.0",
"dependencies": {
"@imgly/background-removal": "^1.7.0",
"chess.js": "^1.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.564.0",
"react": "^19.1.0",
"react-chessboard": "^5.10.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.13.2",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
},
@@ -97,6 +102,14 @@
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
"@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="],
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
@@ -335,12 +348,16 @@
"caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
"chess.js": ["chess.js@1.4.0", "", {}, "sha512-BBJgrrtKQOzFLonR0l+k64A98NLemPwNsCskwb+29bRwobUa4iTm51E1kwGPbWXAcfdDa18nad6vpPPKPWarqw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -437,6 +454,8 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
@@ -467,10 +486,16 @@
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-chessboard": ["react-chessboard@5.10.0", "", { "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-Y3PgaCVhnDG3IaQfu86OzTSEIEAUtuU5XwmHWnx3tcFOX7lSoAq81ZFX3MBj6y5a6FzDMTczMVmkkrV2CzTrIw=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-router": ["react-router@7.13.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA=="],
"react-router-dom": ["react-router-dom@7.13.2", "", { "dependencies": { "react-router": "7.13.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
@@ -479,6 +504,8 @@
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],

View File

@@ -1,492 +0,0 @@
# Aurora API Reference
REST API server for Aurora bot management. Base URL: `http://localhost:3000`
## Common Response Formats
**Success Responses:**
- Single resource: `{ ...resource }` or `{ success: true, resource: {...} }`
- List operations: `{ items: [...], total: number }`
- Mutations: `{ success: true, resource: {...} }`
**Error Responses:**
```json
{
"error": "Brief error message",
"details": "Optional detailed error information"
}
```
**HTTP Status Codes:**
| Code | Description |
|------|-------------|
| 200 | Success |
| 201 | Created |
| 204 | No Content (successful DELETE) |
| 400 | Bad Request (validation error) |
| 404 | Not Found |
| 409 | Conflict (e.g., duplicate name) |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
---
## Health
### `GET /api/health`
Returns server health status.
**Response:** `{ "status": "ok", "timestamp": 1234567890 }`
---
## Items
### `GET /api/items`
List all items with optional filtering.
| Query Param | Type | Description |
|-------------|------|-------------|
| `search` | string | Filter by name/description |
| `type` | string | Filter by item type |
| `rarity` | string | Filter by rarity (C, R, SR, SSR) |
| `limit` | number | Max results (default: 100) |
| `offset` | number | Pagination offset |
**Response:** `{ "items": [...], "total": number }`
### `GET /api/items/:id`
Get single item by ID.
**Response:**
```json
{
"id": 1,
"name": "Health Potion",
"description": "Restores HP",
"type": "CONSUMABLE",
"rarity": "C",
"price": "100",
"iconUrl": "/assets/items/1.png",
"imageUrl": "/assets/items/1.png",
"usageData": { "consume": true, "effects": [] }
}
```
### `POST /api/items`
Create new item. Supports JSON or multipart/form-data with image.
**Body (JSON):**
```json
{
"name": "Health Potion",
"description": "Restores HP",
"type": "CONSUMABLE",
"rarity": "C",
"price": "100",
"iconUrl": "/assets/items/placeholder.png",
"imageUrl": "/assets/items/placeholder.png",
"usageData": { "consume": true, "effects": [] }
}
```
**Body (Multipart):**
- `data`: JSON string with item fields
- `image`: Image file (PNG, JPEG, WebP, GIF, max 15MB)
### `PUT /api/items/:id`
Update existing item.
### `DELETE /api/items/:id`
Delete item and associated asset.
### `POST /api/items/:id/icon`
Upload/replace item image. Accepts multipart/form-data with `image` field.
---
## Users
### `GET /api/users`
List all users with optional filtering and sorting.
| Query Param | Type | Description |
|-------------|------|-------------|
| `search` | string | Filter by username (partial match) |
| `sortBy` | string | Sort field: `balance`, `level`, `xp`, `username` (default: `balance`) |
| `sortOrder` | string | Sort order: `asc`, `desc` (default: `desc`) |
| `limit` | number | Max results (default: 50) |
| `offset` | number | Pagination offset |
**Response:** `{ "users": [...], "total": number }`
### `GET /api/users/:id`
Get single user by Discord ID.
**Response:**
```json
{
"id": "123456789012345678",
"username": "Player1",
"balance": "1000",
"xp": "500",
"level": 5,
"dailyStreak": 3,
"isActive": true,
"classId": "1",
"class": { "id": "1", "name": "Warrior", "balance": "5000" },
"settings": {},
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-15T12:00:00Z"
}
```
### `PUT /api/users/:id`
Update user fields.
**Body:**
```json
{
"username": "NewName",
"balance": "2000",
"xp": "750",
"level": 10,
"dailyStreak": 5,
"classId": "1",
"isActive": true,
"settings": {}
}
```
### `GET /api/users/:id/inventory`
Get user's inventory with item details.
**Response:**
```json
{
"inventory": [
{
"userId": "123456789012345678",
"itemId": 1,
"quantity": "5",
"item": { "id": 1, "name": "Health Potion", ... }
}
]
}
```
### `POST /api/users/:id/inventory`
Add item to user inventory.
**Body:**
```json
{
"itemId": 1,
"quantity": "5"
}
```
### `DELETE /api/users/:id/inventory/:itemId`
Remove item from user inventory. Use query param `amount` to specify quantity (default: 1).
| Query Param | Type | Description |
|-------------|------|-------------|
| `amount` | number | Amount to remove (default: 1) |
```
---
## Classes
### `GET /api/classes`
List all classes.
**Response:**
```json
{
"classes": [
{ "id": "1", "name": "Warrior", "balance": "5000", "roleId": "123456789" }
]
}
```
### `POST /api/classes`
Create new class.
**Body:**
```json
{
"id": "2",
"name": "Mage",
"balance": "0",
"roleId": "987654321"
}
```
### `PUT /api/classes/:id`
Update class.
**Body:**
```json
{
"name": "Updated Name",
"balance": "10000",
"roleId": "111222333"
}
```
### `DELETE /api/classes/:id`
Delete class.
---
## Moderation
### `GET /api/moderation`
List moderation cases with optional filtering.
| Query Param | Type | Description |
|-------------|------|-------------|
| `userId` | string | Filter by target user ID |
| `moderatorId` | string | Filter by moderator ID |
| `type` | string | Filter by case type: `warn`, `timeout`, `kick`, `ban`, `note`, `prune` |
| `active` | boolean | Filter by active status |
| `limit` | number | Max results (default: 50) |
| `offset` | number | Pagination offset |
**Response:**
```json
{
"cases": [
{
"id": "1",
"caseId": "CASE-0001",
"type": "warn",
"userId": "123456789",
"username": "User1",
"moderatorId": "987654321",
"moderatorName": "Mod1",
"reason": "Spam",
"metadata": {},
"active": true,
"createdAt": "2024-01-15T12:00:00Z",
"resolvedAt": null,
"resolvedBy": null,
"resolvedReason": null
}
]
}
```
### `GET /api/moderation/:caseId`
Get single case by case ID (e.g., `CASE-0001`).
### `POST /api/moderation`
Create new moderation case.
**Body:**
```json
{
"type": "warn",
"userId": "123456789",
"username": "User1",
"moderatorId": "987654321",
"moderatorName": "Mod1",
"reason": "Rule violation",
"metadata": { "duration": "24h" }
}
```
### `PUT /api/moderation/:caseId/clear`
Clear/resolve a moderation case.
**Body:**
```json
{
"clearedBy": "987654321",
"clearedByName": "Mod1",
"reason": "Appeal accepted"
}
```
---
## Transactions
### `GET /api/transactions`
List economy transactions.
| Query Param | Type | Description |
|-------------|------|-------------|
| `userId` | string | Filter by user ID |
| `type` | string | Filter by transaction type |
| `limit` | number | Max results (default: 50) |
| `offset` | number | Pagination offset |
**Response:**
```json
{
"transactions": [
{
"id": "1",
"userId": "123456789",
"relatedUserId": null,
"amount": "100",
"type": "DAILY_REWARD",
"description": "Daily reward (Streak: 3)",
"createdAt": "2024-01-15T12:00:00Z"
}
]
}
```
**Transaction Types:**
- `DAILY_REWARD` - Daily claim reward
- `TRANSFER_IN` - Received from another user
- `TRANSFER_OUT` - Sent to another user
- `LOOTDROP_CLAIM` - Claimed lootdrop
- `SHOP_BUY` - Item purchase
- `QUEST_REWARD` - Quest completion reward
---
---
## Lootdrops
### `GET /api/lootdrops`
List lootdrops (default limit 50, sorted by newest).
| Query Param | Type | Description |
|-------------|------|-------------|
| `limit` | number | Max results (default: 50) |
**Response:** `{ "lootdrops": [...] }`
### `POST /api/lootdrops`
Spawn a lootdrop in a channel.
**Body:**
```json
{
"channelId": "1234567890",
"amount": 100,
"currency": "Gold"
}
```
### `DELETE /api/lootdrops/:messageId`
Cancel and delete a lootdrop.
---
## Quests
### `GET /api/quests`
List all quests.
**Response:**
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "Daily Login",
"description": "Login once",
"triggerEvent": "login",
"requirements": { "target": 1 },
"rewards": { "xp": 50, "balance": 100 }
}
]
}
```
### `POST /api/quests`
Create new quest.
**Body:**
```json
{
"name": "Daily Login",
"description": "Login once",
"triggerEvent": "login",
"target": 1,
"xpReward": 50,
"balanceReward": 100
}
```
### `PUT /api/quests/:id`
Update quest.
### `DELETE /api/quests/:id`
Delete quest.
---
## Settings
### `GET /api/settings`
Get current bot configuration.
### `POST /api/settings`
Update configuration (partial merge supported).
### `GET /api/settings/meta`
Get Discord metadata (roles, channels, commands).
**Response:**
```json
{
"roles": [{ "id": "123", "name": "Admin", "color": "#FF0000" }],
"channels": [{ "id": "456", "name": "general", "type": 0 }],
"commands": [{ "name": "daily", "category": "economy" }]
}
```
---
## Admin Actions
### `POST /api/actions/reload-commands`
Reload bot slash commands.
### `POST /api/actions/clear-cache`
Clear internal caches.
### `POST /api/actions/maintenance-mode`
Toggle maintenance mode.
**Body:** `{ "enabled": true, "reason": "Updating..." }`
---
## Stats
### `GET /api/stats`
Get full dashboard statistics.
### `GET /api/stats/activity`
Get activity aggregation (cached 5 min).
---
## Assets
### `GET /assets/items/:filename`
Serve item images. Cached 24 hours.
---
## WebSocket
### `ws://localhost:3000/ws`
Real-time dashboard updates.
**Messages:**
- `STATS_UPDATE` - Periodic stats broadcast (every 5s when clients connected)
- `NEW_EVENT` - Real-time system events
- `PING/PONG` - Heartbeat
**Limits:** Max 10 concurrent connections, 16KB max payload, 60s idle timeout.

View File

@@ -1,769 +0,0 @@
# Aurora Admin Panel - Design Guidelines
## Design Philosophy
The Aurora Admin Panel embodies the intersection of celestial mystique and institutional precision. It is a command center for academy administration—powerful, sophisticated, and unmistakably authoritative. Every interface element should communicate control, clarity, and prestige.
**Core Principles:**
- **Authority over Friendliness**: This is an administrative tool, not a consumer app
- **Data Clarity**: Information density balanced with elegant presentation
- **Celestial Aesthetic**: Subtle cosmic theming that doesn't compromise functionality
- **Institutional Grade**: Professional, trustworthy, built to manage complex systems
---
## Visual Foundation
### Color System
**Background Hierarchy**
```
Level 0 (Base) #0A0A0F Eclipse Void - Deepest background
Level 1 (Container) #151520 Midnight Canvas - Cards, panels, modals
Level 2 (Surface) #1E1B4B Nebula Surface - Elevated elements
Level 3 (Raised) #2D2A5F Stellar Overlay - Hover states, dropdowns
```
**Text Hierarchy**
```
Primary Text #F9FAFB Starlight White - Headings, key data
Secondary Text #E5E7EB Stardust Silver - Body text, labels
Tertiary Text #9CA3AF Cosmic Gray - Helper text, timestamps
Disabled Text #6B7280 Void Gray - Inactive elements
```
**Brand Accents**
```
Primary (Action) #8B5CF6 Aurora Purple - Primary buttons, links, active states
Secondary (Info) #3B82F6 Nebula Blue - Informational elements
Success #10B981 Emerald - Confirmations, positive indicators
Warning #F59E0B Amber - Cautions, alerts
Danger #DC2626 Crimson - Errors, destructive actions
Gold (Prestige) #FCD34D Celestial Gold - Premium features, highlights
```
**Constellation Tier Colors** (for data visualization)
```
Constellation A #FCD34D Celestial Gold
Constellation B #8B5CF6 Aurora Purple
Constellation C #3B82F6 Nebula Blue
Constellation D #6B7280 Slate Gray
```
**Semantic Colors**
```
Currency (AU) #FCD34D Gold - Astral Units indicators
Currency (CU) #8B5CF6 Purple - Constellation Units indicators
XP/Progress #3B82F6 Blue - Experience and progression
Activity #10B981 Green - Active users, live events
```
### Typography
**Font Stack**
Primary (UI Text):
```css
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
```
- Clean, highly legible, modern
- Excellent at small sizes for data-dense interfaces
- Professional without being sterile
Display (Headings):
```css
font-family: 'Space Grotesk', 'Inter', sans-serif;
```
- Geometric, slightly futuristic
- Use for page titles, section headers
- Reinforces celestial/institutional theme
Monospace (Data):
```css
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
```
- For numerical data, timestamps, IDs
- Improves scanability of tabular data
- Technical credibility
**Type Scale**
```
Display Large 48px / 3rem font-weight: 700 (Dashboard headers)
Display 36px / 2.25rem font-weight: 700 (Page titles)
Heading 1 30px / 1.875rem font-weight: 600 (Section titles)
Heading 2 24px / 1.5rem font-weight: 600 (Card headers)
Heading 3 20px / 1.25rem font-weight: 600 (Subsections)
Body Large 16px / 1rem font-weight: 400 (Emphasized body)
Body 14px / 0.875rem font-weight: 400 (Default text)
Body Small 13px / 0.8125rem font-weight: 400 (Secondary info)
Caption 12px / 0.75rem font-weight: 400 (Labels, hints)
Overline 11px / 0.6875rem font-weight: 600 (Uppercase labels)
```
**Font Weight Usage**
- **700 (Bold)**: Display text, critical metrics
- **600 (Semibold)**: Headings, emphasized data
- **500 (Medium)**: Buttons, active tabs, selected items
- **400 (Regular)**: Body text, form inputs
- **Never use weights below 400** - maintain readability
### Spacing & Layout
**Base Unit**: 4px
**Spacing Scale**
```
xs 4px 0.25rem Tight spacing, icon gaps
sm 8px 0.5rem Form element spacing
md 16px 1rem Default component spacing
lg 24px 1.5rem Section spacing
xl 32px 2rem Major section breaks
2xl 48px 3rem Page section dividers
3xl 64px 4rem Major layout divisions
```
**Container Widths**
```
Full Bleed 100% Full viewport width
Wide 1600px Wide dashboards, data tables
Standard 1280px Default content width
Narrow 960px Forms, focused content
Reading 720px Long-form text (documentation)
```
**Grid System**
- 12-column grid for flexible layouts
- 24px gutters between columns
- Responsive breakpoints: 640px, 768px, 1024px, 1280px, 1536px
### Borders & Dividers
**Border Widths**
```
Hairline 0.5px Subtle dividers
Thin 1px Default borders
Medium 2px Emphasized borders, focus states
Thick 4px Accent bars, category indicators
```
**Border Colors**
```
Default #2D2A5F 15% opacity - Standard dividers
Subtle #2D2A5F 8% opacity - Very light separation
Emphasized #8B5CF6 30% opacity - Highlighted borders
Interactive #8B5CF6 60% opacity - Hover/focus states
```
**Border Radius**
```
None 0px Data tables, strict layouts
sm 4px Buttons, badges, pills
md 8px Cards, inputs, panels
lg 12px Large cards, modals
xl 16px Feature cards, images
2xl 24px Hero elements
full 9999px Circular elements, avatars
```
---
## UI Patterns
### Cards & Containers
**Standard Card**
```
Background: #151520 (Midnight Canvas)
Border: 1px solid rgba(139, 92, 246, 0.15)
Border Radius: 8px
Padding: 24px
Shadow: 0 4px 16px rgba(0, 0, 0, 0.4)
```
**Elevated Card** (hover/focus)
```
Background: #1E1B4B (Nebula Surface)
Border: 1px solid rgba(139, 92, 246, 0.3)
Shadow: 0 8px 24px rgba(0, 0, 0, 0.6)
Transform: translateY(-2px)
Transition: all 200ms ease
```
**Stat Card** (metrics, KPIs)
```
Background: Linear gradient from #151520 to #1E1B4B
Border: 1px solid rgba(139, 92, 246, 0.2)
Accent Border: 4px left border in tier/category color
Icon: Celestial icon in accent color
Typography: Large number (Display), small label (Overline)
```
### Data Tables
**Table Structure**
```
Header Background: #1E1B4B
Header Text: #E5E7EB, 11px uppercase, 600 weight
Row Background: Alternating #0A0A0F / #151520
Row Hover: #2D2A5F with 40% opacity
Border: 1px solid rgba(139, 92, 246, 0.1) between rows
Cell Padding: 12px 16px
```
**Column Styling**
- Left-align text columns
- Right-align numerical columns
- Monospace font for numbers, IDs, timestamps
- Icon + text combinations for status indicators
**Interactive Elements**
- Sortable headers with subtle arrow icons
- Hover state on entire row
- Click/select highlight with Aurora Purple tint
- Pagination in Nebula Blue
### Forms & Inputs
**Input Fields**
```
Background: #1E1B4B
Border: 1px solid rgba(139, 92, 246, 0.2)
Border Radius: 6px
Padding: 10px 14px
Font Size: 14px
Text Color: #F9FAFB
Focus State:
Border: 2px solid #8B5CF6
Glow: 0 0 0 3px rgba(139, 92, 246, 0.2)
Error State:
Border: 1px solid #DC2626
Text: #DC2626 helper text below
Disabled State:
Background: #0A0A0F
Text: #6B7280
Cursor: not-allowed
```
**Labels**
```
Font Size: 12px
Font Weight: 600
Text Color: #E5E7EB
Margin Bottom: 6px
```
**Select Dropdowns**
```
Same base styling as inputs
Dropdown Icon: Chevron in #9CA3AF
Menu Background: #2D2A5F
Menu Border: 1px solid rgba(139, 92, 246, 0.3)
Option Hover: #3B82F6 background
Selected: #8B5CF6 with checkmark icon
```
**Checkboxes & Radio Buttons**
```
Size: 18px × 18px
Border: 2px solid rgba(139, 92, 246, 0.4)
Border Radius: 4px (checkbox) / 50% (radio)
Checked: #8B5CF6 background with white checkmark
Hover: Glow effect rgba(139, 92, 246, 0.2)
```
### Buttons
**Primary Button**
```
Background: #8B5CF6 (Aurora Purple)
Text: #FFFFFF
Padding: 10px 20px
Border Radius: 6px
Font Weight: 500
Shadow: 0 2px 8px rgba(139, 92, 246, 0.3)
Hover:
Background: #7C3AED (lighter purple)
Shadow: 0 4px 12px rgba(139, 92, 246, 0.4)
Active:
Background: #6D28D9 (darker purple)
Transform: scale(0.98)
```
**Secondary Button**
```
Background: transparent
Border: 1px solid rgba(139, 92, 246, 0.5)
Text: #8B5CF6
Padding: 10px 20px
Hover:
Background: rgba(139, 92, 246, 0.1)
Border: 1px solid #8B5CF6
```
**Destructive Button**
```
Background: #DC2626
Text: #FFFFFF
(Same structure as Primary)
```
**Ghost Button**
```
Background: transparent
Text: #E5E7EB
Padding: 8px 16px
Hover:
Background: rgba(139, 92, 246, 0.1)
Text: #8B5CF6
```
**Button Sizes**
```
Small 8px 12px 12px text
Medium 10px 20px 14px text (default)
Large 12px 24px 16px text
```
### Navigation
**Sidebar Navigation**
```
Background: #0A0A0F with subtle gradient
Width: 260px (expanded) / 64px (collapsed)
Border Right: 1px solid rgba(139, 92, 246, 0.15)
Nav Item:
Padding: 12px 16px
Border Radius: 6px
Font Size: 14px
Font Weight: 500
Icon Size: 20px
Gap: 12px between icon and text
Active State:
Background: rgba(139, 92, 246, 0.15)
Border Left: 4px solid #8B5CF6
Text: #8B5CF6
Icon: #8B5CF6
Hover State:
Background: rgba(139, 92, 246, 0.08)
Text: #F9FAFB
```
**Top Bar / Header**
```
Background: #0A0A0F with backdrop blur
Height: 64px
Border Bottom: 1px solid rgba(139, 92, 246, 0.15)
Position: Sticky
Z-index: 100
Contains:
- Logo / Academy name
- Global search
- Quick actions
- User profile dropdown
- Notification bell
```
**Breadcrumbs**
```
Font Size: 13px
Text Color: #9CA3AF
Separator: "/" or "" in #6B7280
Current Page: #F9FAFB, 600 weight
Links: #9CA3AF, hover to #8B5CF6
```
### Modals & Overlays
**Modal Structure**
```
Backdrop: rgba(0, 0, 0, 0.8) with backdrop blur
Modal Container: #151520
Border: 1px solid rgba(139, 92, 246, 0.2)
Border Radius: 12px
Shadow: 0 24px 48px rgba(0, 0, 0, 0.9)
Max Width: 600px (standard) / 900px (wide)
Padding: 32px
Header:
Border Bottom: 1px solid rgba(139, 92, 246, 0.15)
Padding: 0 0 20px 0
Font Size: 24px
Font Weight: 600
Footer:
Border Top: 1px solid rgba(139, 92, 246, 0.15)
Padding: 20px 0 0 0
Buttons: Right-aligned, 12px gap
```
**Toast Notifications**
```
Position: Top-right, 24px margin
Background: #2D2A5F
Border: 1px solid (color based on type)
Border Radius: 8px
Padding: 16px 20px
Max Width: 400px
Shadow: 0 8px 24px rgba(0, 0, 0, 0.6)
Success: #10B981 border, green icon
Warning: #F59E0B border, amber icon
Error: #DC2626 border, red icon
Info: #3B82F6 border, blue icon
Animation: Slide in from right, fade out
Duration: 4 seconds (dismissible)
```
### Data Visualization
**Charts & Graphs**
```
Background: #151520 or transparent
Grid Lines: rgba(139, 92, 246, 0.1)
Axis Labels: #9CA3AF, 12px
Data Points: Constellation tier colors or semantic colors
Tooltips: #2D2A5F background, white text
Legend: Horizontal, 12px, icons + labels
```
**Progress Bars**
```
Track: #1E1B4B
Fill: Linear gradient with tier/category color
Height: 8px (thin) / 12px (medium) / 16px (thick)
Border Radius: 9999px
Label: Above or inline, monospace numbers
```
**Badges & Pills**
```
Background: Semantic color with 15% opacity
Text: Semantic color (full saturation)
Border: 1px solid semantic color with 30% opacity
Padding: 4px 10px
Border Radius: 9999px
Font Size: 12px
Font Weight: 500
Status Examples:
Active: Green
Pending: Amber
Inactive: Gray
Error: Red
Premium: Gold
```
### Icons
**Icon System**
- Use consistent icon family (e.g., Lucide, Heroicons, Phosphor)
- Line-style icons, not filled (except for active states)
- Stroke width: 1.5px-2px
- Sizes: 16px (small), 20px (default), 24px (large), 32px (extra large)
**Icon Colors**
- Default: #9CA3AF (Cosmic Gray)
- Active/Selected: #8B5CF6 (Aurora Purple)
- Success: #10B981
- Warning: #F59E0B
- Error: #DC2626
**Celestial Icon Themes**
- Stars, constellations, orbits for branding
- Minimalist, geometric line art
- Avoid overly detailed or realistic astronomy images
---
## Animation & Motion
### Principles
- **Purposeful**: Animations guide attention and provide feedback
- **Subtle**: No distracting or excessive motion
- **Fast**: Snappy interactions (150-300ms)
- **Professional**: Ease curves that feel polished
### Timing Functions
```
ease-out Default for most interactions
ease-in-out Modal/panel transitions
ease-in Exit animations
spring Micro-interactions (subtle bounce)
```
### Standard Durations
```
Instant 0ms State changes
Fast 150ms Button hover, color changes
Standard 200ms Card hover, dropdown open
Moderate 300ms Modal open, page transitions
Slow 500ms Large panel animations
```
### Common Animations
**Hover Effects**
```css
transition: all 200ms ease-out;
transform: translateY(-2px);
box-shadow: [enhanced shadow];
```
**Focus States**
```css
transition: border 150ms ease-out, box-shadow 150ms ease-out;
border-color: #8B5CF6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
```
**Loading States**
```
Skeleton: Shimmer effect from left to right
Spinner: Rotating celestial icon or ring
Progress: Smooth bar fill with easing
```
**Page Transitions**
```
Fade in: Opacity 0 → 1 over 200ms
Slide up: TranslateY(20px) → 0 over 300ms
Blur fade: Blur + opacity for backdrop
```
---
## Responsive Behavior
### Breakpoints
```
Mobile < 640px
Tablet 640px - 1024px
Desktop > 1024px
Wide Desktop > 1536px
```
### Mobile Adaptations
- Sidebar collapses to hamburger menu
- Cards stack vertically
- Tables become horizontally scrollable or convert to card view
- Reduce padding and spacing by 25-50%
- Larger touch targets (minimum 44px)
- Bottom navigation for primary actions
### Tablet Optimizations
- Hybrid layouts (sidebar can be toggled)
- Adaptive grid (4 columns → 2 columns)
- Touch-friendly sizing maintained
- Utilize available space efficiently
---
## Accessibility
### Color Contrast
- Maintain WCAG AA standards minimum (4.5:1 for normal text)
- Critical actions and text meet AAA standards (7:1)
- Never rely on color alone for information
### Focus Indicators
- Always visible focus states
- 2px Aurora Purple outline with 3px glow
- Logical tab order follows visual hierarchy
### Screen Readers
- Semantic HTML structure
- ARIA labels for icon-only buttons
- Status messages announced appropriately
- Table headers properly associated
### Keyboard Navigation
- All interactive elements accessible via keyboard
- Modal traps focus within itself
- Escape key closes overlays
- Arrow keys for navigation where appropriate
---
## Dark Mode Philosophy
**Aurora Admin is dark-first by design.** The interface assumes a dark environment and doesn't offer a light mode toggle. This decision is intentional:
- **Focus**: Dark reduces eye strain during extended admin sessions
- **Data Emphasis**: Light text on dark makes numbers/data more prominent
- **Celestial Theme**: Dark backgrounds reinforce the cosmic aesthetic
- **Professional**: Dark UIs feel more serious and technical
If light mode is ever required, avoid pure white—use off-white (#F9FAFB) backgrounds with careful contrast management.
---
## Theming & Customization
### Constellation Tier Theming
When displaying constellation-specific data:
- Use tier colors for accents, not backgrounds
- Apply colors to borders, icons, badges
- Maintain readability—don't overwhelm with color
### Admin Privilege Levels
Different admin roles can have subtle UI indicators:
- Super Admin: Gold accents
- Moderator: Purple accents
- Viewer: Blue accents
These are subtle hints, not dominant visual themes.
---
## Component Library Standards
### Consistency
- Reuse components extensively
- Maintain consistent spacing, sizing, behavior
- Document component variants clearly
- Avoid one-off custom elements
### Composability
- Build complex UIs from simple components
- Components should work together seamlessly
- Predictable prop APIs
- Flexible but not overly configurable
### Performance
- Lazy load heavy components
- Virtualize long lists
- Optimize re-renders
- Compress and cache assets
---
## Code Style (UI Framework Agnostic)
### Class Naming
Use clear, semantic names:
```
.card-stat Not .cs or .c1
.button-primary Not .btn-p or .bp
.table-header Not .th or .t-h
```
### Component Organization
```
/components
/ui Base components (buttons, inputs)
/layout Layout components (sidebar, header)
/data Data components (tables, charts)
/feedback Toasts, modals, alerts
/forms Form-specific components
```
### Style Organization
- Variables/tokens for all design values
- No magic numbers in components
- DRY—reuse common styles
- Mobile-first responsive approach
---
## Best Practices
### Do's ✓
- Use established patterns from these guidelines
- Maintain consistent spacing throughout
- Prioritize data clarity and scannability
- Test with real data, not lorem ipsum
- Consider loading and empty states
- Provide clear feedback for all actions
- Use progressive disclosure for complex features
### Don'ts ✗
- Don't use bright, saturated colors outside defined palette
- Don't create custom components when standard ones exist
- Don't sacrifice accessibility for aesthetics
- Don't use decorative animations that distract
- Don't hide critical actions in nested menus
- Don't use tiny fonts (below 12px) for functional text
- Don't ignore error states and edge cases
---
## Quality Checklist
Before considering any UI complete:
**Visual**
- [ ] Colors match defined palette exactly
- [ ] Spacing uses the 4px grid system
- [ ] Typography follows scale and hierarchy
- [ ] Borders and shadows are consistent
- [ ] Icons are properly sized and aligned
**Interaction**
- [ ] Hover states are defined for all interactive elements
- [ ] Focus states are visible and clear
- [ ] Loading states prevent user confusion
- [ ] Success/error feedback is immediate
- [ ] Animations are smooth and purposeful
**Responsive**
- [ ] Layout adapts to mobile, tablet, desktop
- [ ] Touch targets are minimum 44px on mobile
- [ ] Text remains readable at all sizes
- [ ] No horizontal scrolling (except intentional)
**Accessibility**
- [ ] Keyboard navigation works completely
- [ ] Focus indicators are always visible
- [ ] Color contrast meets WCAG AA minimum
- [ ] ARIA labels present where needed
- [ ] Screen reader tested for critical flows
**Data**
- [ ] Empty states are handled gracefully
- [ ] Error states provide actionable guidance
- [ ] Large datasets perform well
- [ ] Loading states prevent layout shift
---
## Reference Assets
### Suggested Icon Library
- **Lucide Icons**: Clean, consistent, extensive
- **Heroicons**: Tailwind-friendly, well-designed
- **Phosphor Icons**: Flexible weights and styles
### Font Resources
- **Inter**: [Google Fonts](https://fonts.google.com/specimen/Inter)
- **Space Grotesk**: [Google Fonts](https://fonts.google.com/specimen/Space+Grotesk)
- **JetBrains Mono**: [JetBrains](https://www.jetbrains.com/lp/mono/)
### Design Tools
- Use component libraries: shadcn/ui, Headless UI, Radix
- Tailwind CSS for utility-first styling
- CSS variables for theming
- Design tokens for consistency
---
## Conclusion
The Aurora Admin Panel is a sophisticated tool that demands respect through its design. Every pixel serves a purpose—whether to inform, to guide, or to reinforce the prestige of the academy it administers.
**Design with authority. Build with precision. Maintain the standard.**
---
*These design guidelines are living documentation. As Aurora evolves, so too should these standards. Propose updates through the standard development workflow.*

View File

@@ -1,168 +0,0 @@
# Feature Flag System
The feature flag system enables controlled beta testing of new features in production without requiring a separate test environment.
## Overview
Feature flags allow you to:
- Test new features with a limited audience before full rollout
- Enable/disable features without code changes or redeployment
- Control access per guild, user, or role
- Eliminate environment drift between test and production
## Architecture
### Database Schema
**`feature_flags` table:**
| Column | Type | Description |
|--------|------|-------------|
| `id` | serial | Primary key |
| `name` | varchar(100) | Unique flag identifier |
| `enabled` | boolean | Whether the flag is active |
| `description` | text | Human-readable description |
| `created_at` | timestamp | Creation time |
| `updated_at` | timestamp | Last update time |
**`feature_flag_access` table:**
| Column | Type | Description |
|--------|------|-------------|
| `id` | serial | Primary key |
| `flag_id` | integer | References feature_flags.id |
| `guild_id` | bigint | Guild whitelist (nullable) |
| `user_id` | bigint | User whitelist (nullable) |
| `role_id` | bigint | Role whitelist (nullable) |
| `created_at` | timestamp | Creation time |
### Service Layer
The `featureFlagsService` (`shared/modules/feature-flags/feature-flags.service.ts`) provides:
```typescript
// Check if a flag is globally enabled
await featureFlagsService.isFlagEnabled("trading_system");
// Check if a user has access to a flagged feature
await featureFlagsService.hasAccess("trading_system", {
guildId: "123456789",
userId: "987654321",
memberRoles: ["role1", "role2"]
});
// Create a new feature flag
await featureFlagsService.createFlag("new_feature", "Description");
// Enable/disable a flag
await featureFlagsService.setFlagEnabled("new_feature", true);
// Grant access to users/roles/guilds
await featureFlagsService.grantAccess("new_feature", { userId: "123" });
await featureFlagsService.grantAccess("new_feature", { roleId: "456" });
await featureFlagsService.grantAccess("new_feature", { guildId: "789" });
// List all flags or access records
await featureFlagsService.listFlags();
await featureFlagsService.listAccess("new_feature");
```
## Usage
### Marking a Command as Beta
Add `beta: true` to any command definition:
```typescript
export const newFeature = createCommand({
data: new SlashCommandBuilder()
.setName("newfeature")
.setDescription("A new experimental feature"),
beta: true, // Marks this command as a beta feature
execute: async (interaction) => {
// Implementation
},
});
```
By default, the command name is used as the feature flag name. To use a custom flag name:
```typescript
export const trade = createCommand({
data: new SlashCommandBuilder()
.setName("trade")
.setDescription("Trade items with another user"),
beta: true,
featureFlag: "trading_system", // Custom flag name
execute: async (interaction) => {
// Implementation
},
});
```
### Access Control Flow
When a user attempts to use a beta command:
1. **Check if flag exists and is enabled** - Returns false if flag doesn't exist or is disabled
2. **Check guild whitelist** - User's guild has access if `guild_id` matches
3. **Check user whitelist** - User has access if `user_id` matches
4. **Check role whitelist** - User has access if any of their roles match
If none of these conditions are met, the user sees:
> **Beta Feature**
> This feature is currently in beta testing and not available to all users. Stay tuned for the official release!
## Admin Commands
The `/featureflags` command (Administrator only) provides full management:
### Subcommands
| Command | Description |
|---------|-------------|
| `/featureflags list` | List all feature flags with status |
| `/featureflags create <name> [description]` | Create a new flag (disabled by default) |
| `/featureflags delete <name>` | Delete a flag and all access records |
| `/featureflags enable <name>` | Enable a flag globally |
| `/featureflags disable <name>` | Disable a flag globally |
| `/featureflags grant <name> [user\|role]` | Grant access to a user or role |
| `/featureflags revoke <id>` | Revoke access by record ID |
| `/featureflags access <name>` | List all access records for a flag |
### Example Workflow
```
1. Create the flag:
/featureflags create trading_system "Item trading between users"
2. Grant access to beta testers:
/featureflags grant trading_system user:@beta_tester
/featureflags grant trading_system role:@Beta Testers
3. Enable the flag:
/featureflags enable trading_system
4. View access list:
/featureflags access trading_system
5. When ready for full release:
- Remove beta: true from the command
- Delete the flag: /featureflags delete trading_system
```
## Best Practices
1. **Descriptive Names**: Use snake_case names that clearly describe the feature
2. **Document Flags**: Always add a description when creating flags
3. **Role-Based Access**: Prefer role-based access over user-based for easier management
4. **Clean Up**: Delete flags after features are fully released
5. **Testing**: Always test with a small group before wider rollout
## Implementation Files
| File | Purpose |
|------|---------|
| `shared/db/schema/feature-flags.ts` | Database schema |
| `shared/modules/feature-flags/feature-flags.service.ts` | Service layer |
| `shared/lib/types.ts` | Command interface with beta properties |
| `bot/lib/handlers/CommandHandler.ts` | Beta access check |
| `bot/commands/admin/featureflags.ts` | Admin command |

View File

@@ -1,199 +0,0 @@
# Guild Settings System
The guild settings system enables per-guild configuration stored in the database, eliminating environment-specific config files and enabling runtime updates without redeployment.
## Overview
Guild settings allow you to:
- Store per-guild configuration in the database
- Update settings at runtime without code changes
- Support multiple guilds with different configurations
- Maintain backward compatibility with file-based config
## Architecture
### Database Schema
**`guild_settings` table:**
| Column | Type | Description |
|--------|------|-------------|
| `guild_id` | bigint | Primary key (Discord guild ID) |
| `student_role_id` | bigint | Student role ID |
| `visitor_role_id` | bigint | Visitor role ID |
| `color_role_ids` | jsonb | Array of color role IDs |
| `welcome_channel_id` | bigint | Welcome message channel |
| `welcome_message` | text | Custom welcome message |
| `feedback_channel_id` | bigint | Feedback channel |
| `terminal_channel_id` | bigint | Terminal channel |
| `terminal_message_id` | bigint | Terminal message ID |
| `moderation_log_channel_id` | bigint | Moderation log channel |
| `moderation_dm_on_warn` | jsonb | DM user on warn |
| `moderation_auto_timeout_threshold` | jsonb | Auto timeout after N warnings |
| `feature_overrides` | jsonb | Feature flag overrides |
| `created_at` | timestamp | Creation time |
| `updated_at` | timestamp | Last update time |
### Service Layer
The `guildSettingsService` (`shared/modules/guild-settings/guild-settings.service.ts`) provides:
```typescript
// Get settings for a guild (returns null if not configured)
await guildSettingsService.getSettings(guildId);
// Create or update settings
await guildSettingsService.upsertSettings({
guildId: "123456789",
studentRoleId: "987654321",
visitorRoleId: "111222333",
});
// Update a single setting
await guildSettingsService.updateSetting(guildId, "welcomeChannel", "456789123");
// Delete all settings for a guild
await guildSettingsService.deleteSettings(guildId);
// Color role helpers
await guildSettingsService.addColorRole(guildId, roleId);
await guildSettingsService.removeColorRole(guildId, roleId);
```
## Usage
### Getting Guild Configuration
Use `getGuildConfig()` instead of direct `config` imports for guild-specific settings:
```typescript
import { getGuildConfig } from "@shared/lib/config";
// In a command or interaction
const guildConfig = await getGuildConfig(interaction.guildId);
// Access settings
const studentRole = guildConfig.studentRole;
const welcomeChannel = guildConfig.welcomeChannelId;
```
### Fallback Behavior
`getGuildConfig()` returns settings in this order:
1. **Database settings** (if guild is configured in DB)
2. **File config fallback** (during migration period)
This ensures backward compatibility while migrating from file-based config.
### Cache Invalidation
Settings are cached for 60 seconds. After updating settings, invalidate the cache:
```typescript
import { invalidateGuildConfigCache } from "@shared/lib/config";
await guildSettingsService.upsertSettings({ guildId, ...settings });
invalidateGuildConfigCache(guildId);
```
## Admin Commands
The `/settings` command (Administrator only) provides full management:
### Subcommands
| Command | Description |
|---------|-------------|
| `/settings show` | Display current guild settings |
| `/settings set <key> [value]` | Update a setting |
| `/settings reset <key>` | Reset a setting to default |
| `/settings colors <action> [role]` | Manage color roles |
### Settable Keys
| Key | Type | Description |
|-----|------|-------------|
| `studentRole` | Role | Role for enrolled students |
| `visitorRole` | Role | Role for visitors |
| `welcomeChannel` | Channel | Channel for welcome messages |
| `welcomeMessage` | Text | Custom welcome message |
| `feedbackChannel` | Channel | Channel for feedback |
| `terminalChannel` | Channel | Terminal channel |
| `terminalMessage` | Text | Terminal message ID |
| `moderationLogChannel` | Channel | Moderation log channel |
| `moderationDmOnWarn` | Boolean | DM users on warn |
| `moderationAutoTimeoutThreshold` | Number | Auto timeout threshold |
## API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/guilds/:guildId/settings` | Get guild settings |
| PUT | `/api/guilds/:guildId/settings` | Create/replace settings |
| PATCH | `/api/guilds/:guildId/settings` | Partial update |
| DELETE | `/api/guilds/:guildId/settings` | Delete settings |
## Migration
To migrate existing config.json settings to the database:
```bash
bun run db:migrate-config
```
This will:
1. Read values from `config.json`
2. Create a database record for `DISCORD_GUILD_ID`
3. Store all guild-specific settings
## Migration Strategy for Code
Update code references incrementally:
```typescript
// Before
import { config } from "@shared/lib/config";
const role = config.studentRole;
// After
import { getGuildConfig } from "@shared/lib/config";
const guildConfig = await getGuildConfig(guildId);
const role = guildConfig.studentRole;
```
### Files to Update
Files using guild-specific config that should be updated:
- `bot/events/guildMemberAdd.ts`
- `bot/modules/user/enrollment.interaction.ts`
- `bot/modules/feedback/feedback.interaction.ts`
- `bot/commands/feedback/feedback.ts`
- `bot/commands/inventory/use.ts`
- `bot/commands/admin/create_color.ts`
- `shared/modules/moderation/moderation.service.ts`
- `shared/modules/terminal/terminal.service.ts`
## Files Updated to Use Database Config
All code has been migrated to use `getGuildConfig()`:
- `bot/events/guildMemberAdd.ts` - Role assignment on join
- `bot/modules/user/enrollment.interaction.ts` - Enrollment flow
- `bot/modules/feedback/feedback.interaction.ts` - Feedback submission
- `bot/commands/feedback/feedback.ts` - Feedback command
- `bot/commands/inventory/use.ts` - Color role handling
- `bot/commands/admin/create_color.ts` - Color role creation
- `bot/commands/admin/warn.ts` - Warning with DM and auto-timeout
- `shared/modules/moderation/moderation.service.ts` - Accepts config param
- `shared/modules/terminal/terminal.service.ts` - Terminal location persistence
- `shared/modules/economy/lootdrop.service.ts` - Terminal updates
## Implementation Files
| File | Purpose |
|------|---------|
| `shared/db/schema/guild-settings.ts` | Database schema |
| `shared/modules/guild-settings/guild-settings.service.ts` | Service layer |
| `shared/lib/config.ts` | Config loader with getGuildConfig() |
| `bot/commands/admin/settings.ts` | Admin command |
| `web/src/routes/guild-settings.routes.ts` | API routes |
| `shared/scripts/migrate-config-to-db.ts` | Migration script |

View File

@@ -1,162 +0,0 @@
# Aurora - Discord RPG Bot
A comprehensive, feature-rich Discord RPG bot built with modern technologies using a monorepo architecture.
## Architecture Overview
Aurora uses a **Single Process Monolith** architecture that runs both the Discord bot and REST API in the same Bun process. This design maximizes performance by eliminating inter-process communication overhead and simplifies deployment to a single Docker container.
## Monorepo Structure
```
aurora-bot-discord/
├── bot/ # Discord bot implementation
│ ├── commands/ # Slash command implementations
│ ├── events/ # Discord event handlers
│ ├── lib/ # Bot core logic (BotClient, utilities)
│ └── index.ts # Bot entry point
├── web/ # REST API server
│ └── src/routes/ # API route handlers
├── shared/ # Shared code between bot and web
│ ├── db/ # Database schema and Drizzle ORM
│ ├── lib/ # Utilities, config, logger, events
│ ├── modules/ # Domain services (economy, admin, quest)
│ └── config/ # Configuration files
├── docker-compose.yml # Docker services (app, db)
└── package.json # Root package manifest
```
## Main Application Parts
### 1. Discord Bot (`bot/`)
The bot is built with Discord.js v14 and handles all Discord-related functionality.
**Core Components:**
- **BotClient** (`bot/lib/BotClient.ts`): Central client that manages commands, events, and Discord interactions
- **Commands** (`bot/commands/`): Slash command implementations organized by category:
- `admin/`: Server management commands (config, prune, warnings, notes)
- `economy/`: Economy commands (balance, daily, pay, trade, trivia)
- `inventory/`: Item management commands
- `leveling/`: XP and level tracking
- `quest/`: Quest commands
- `user/`: User profile commands
- **Events** (`bot/events/`): Discord event handlers:
- `interactionCreate.ts`: Command interactions
- `messageCreate.ts`: Message processing
- `ready.ts`: Bot ready events
- `guildMemberAdd.ts`: New member handling
### 2. REST API (`web/`)
A headless REST API built with Bun's native HTTP server for bot administration and data access.
**Key Endpoints:**
- **Stats** (`/api/stats`): Real-time bot metrics and statistics
- **Settings** (`/api/settings`): Configuration management endpoints
- **Users** (`/api/users`): User data and profiles
- **Items** (`/api/items`): Item catalog and management
- **Quests** (`/api/quests`): Quest data and progress
- **Economy** (`/api/transactions`): Economy and transaction data
**API Features:**
- Built with Bun's native HTTP server
- WebSocket support for real-time updates (`/ws`)
- REST API endpoints for all bot data
- Real-time event streaming via WebSocket
- Zod validation for all requests
### 3. Shared Core (`shared/`)
Shared code accessible by both bot and web applications.
**Database Layer (`shared/db/`):**
- **schema.ts**: Drizzle ORM schema definitions for:
- `users`: User profiles with economy data
- `items`: Item catalog with rarities and types
- `inventory`: User item holdings
- `transactions`: Economy transaction history
- `classes`: RPG class system
- `moderationCases`: Moderation logs
- `quests`: Quest definitions
**Modules (`shared/modules/`):**
- **economy/**: Economy service, lootdrops, daily rewards, trading
- **admin/**: Administrative actions (maintenance mode, cache clearing)
- **quest/**: Quest creation and tracking
- **dashboard/**: Dashboard statistics and real-time event bus
- **leveling/**: XP and leveling logic
**Utilities (`shared/lib/`):**
- `config.ts`: Application configuration management
- `logger.ts`: Structured logging system
- `env.ts`: Environment variable handling
- `events.ts`: Event bus for inter-module communication
- `constants.ts`: Application-wide constants
## Main Use-Cases
### For Discord Users
1. **Class System**: Users can join different RPG classes with unique roles
2. **Economy**:
- View balance and net worth
- Earn currency through daily rewards, trivia, and lootdrops
- Send payments to other users
3. **Trading**: Secure trading system between users
4. **Inventory Management**: Collect, use, and trade items with rarities
5. **Leveling**: XP-based progression system tied to activity
6. **Quests**: Complete quests for rewards
7. **Lootdrops**: Random currency drops in text channels
### For Server Administrators
1. **Bot Configuration**: Adjust economy rates, enable/disable features via API
2. **Moderation Tools**:
- Warn, note, and track moderation cases
- Mass prune inactive members
- Role management
3. **Quest Management**: Create and manage server-specific quests
4. **Monitoring**:
- Real-time statistics via REST API
- Activity data and event logs
- Economy leaderboards
### For Developers
1. **Single Process Architecture**: Easy debugging with unified runtime
2. **Type Safety**: Full TypeScript across all modules
3. **Testing**: Bun test framework with unit tests for core services
4. **Docker Support**: Production-ready containerization
5. **Remote Access**: SSH tunneling scripts for production debugging
## Technology Stack
| Layer | Technology |
| ---------------- | --------------------------------- |
| Runtime | Bun 1.0+ |
| Bot Framework | Discord.js 14.x |
| Web Framework | Bun HTTP Server (REST API) |
| Database | PostgreSQL 17 |
| ORM | Drizzle ORM |
| UI | Discord embeds and components |
| Validation | Zod |
| Containerization | Docker |
## Running the Application
```bash
# Database migrations
bun run migrate
# Production (Docker)
docker compose up
```
The bot and API server run on port 3000 and are accessible at `http://localhost:3000`.

60
docs/new-design/DESIGN.md Normal file
View File

@@ -0,0 +1,60 @@
# Stellar Editorial
This document now tracks the design language that is actually implemented in the panel, with a small amount of forward-looking guidance where the code is still catching up.
## Current implementation status
Implemented today in `panel/src/index.css` and the active panel pages:
- dark "void and light" surface stack
- Celestial Gold primary color family
- Noto Serif, Manrope, Space Grotesk, and JetBrains Mono typography
- low-contrast "ghost border" treatment
- rounded surface hierarchy for cards, sidebars, and controls
Partially implemented or still aspirational:
- stronger asymmetry and editorial layouts across more pages
- decorative constellation/nebula treatments
- more consistent premium component states across every admin screen
## Design intent
Aurora's panel should feel like an elite astronomical academy rather than a default admin dashboard. The codebase already follows that direction through the theme tokens and typography system; new UI work should continue that tone instead of falling back to generic SaaS styling.
## Core tokens
Surface hierarchy:
- `--color-background`: `#0d1323`
- `--color-surface-container-low`: `#151b2c`
- `--color-surface-container-high`: `#24293b`
- `--color-surface-container-highest`: `#2f3446`
Primary accents:
- `--color-primary`: `#e9c349`
- `--color-primary-fixed-dim`: `#d4af37`
- `--color-primary-container`: `#3d2e00`
Typography:
- display: Noto Serif
- body: Manrope
- labels: Space Grotesk
- mono: JetBrains Mono
## Component guidance
- Prefer tonal separation over heavy borders.
- Use gold as a focused accent, not a flood color.
- Keep text contrast high and metadata quieter.
- Sidebar, cards, tables, and game views should feel like the same product family.
- Avoid plain white cards, Discord blurple defaults, and generic component-library styling.
## Practical rules for future work
- Start from the CSS tokens in `panel/src/index.css` instead of inventing new one-off colors.
- Preserve the current font roles unless there is a strong reason to change them.
- Use gradients, glow, or tonal depth sparingly and intentionally.
- Keep mobile behavior first-class; the existing layout already has mobile drawer behavior that new pages should respect.

View File

@@ -1,548 +0,0 @@
# Lootbox UX Overhaul Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Overhaul lootbox pull results and shop loot table displays using Discord Components V2 with rarity-driven theming.
**Architecture:** Extract shared rarity config and asset helpers into `shared/lib/rarity.ts`. Modify `effect.handlers.ts` to return separate `iconUrl`/`imageUrl` fields. Rewrite `inventory.view.ts` pull result builder and `shop.view.ts` loot table section to use Components V2 containers with rarity-themed accent colors.
**Tech Stack:** TypeScript, Discord.js (Components V2: ContainerBuilder, SectionBuilder, TextDisplayBuilder, MediaGalleryBuilder, SeparatorBuilder), Bun test
**Spec:** `docs/superpowers/specs/2026-03-18-lootbox-ux-overhaul-design.md`
**Note on color changes:** The new `RARITY_CONFIG` uses slightly different hex values than the previous Discord.js `Colors` enum values for Common (`0x95A5A6` vs `Colors.LightGrey` = `0xBCC0C0`) and Nothing (`0x636363` vs `Colors.DarkButNotBlack` = `0x2C2F33`). This is intentional per the design spec.
**Note on `useItem` return shape:** `inventoryService.useItem()` returns the full item from the Drizzle relation query (`with: { item: true }`), which already includes both `iconUrl` and `imageUrl` columns from the `items` schema. No changes to the service are needed.
---
## File Structure
| File | Action | Responsibility |
|------|--------|----------------|
| `shared/lib/rarity.ts` | Create | `RARITY_CONFIG` map, `defaultName` helper, rarity lookup with fallback |
| `shared/lib/rarity.test.ts` | Create | Tests for rarity config lookup and defaultName |
| `shared/modules/inventory/effect.handlers.ts` | Modify | Return separate `iconUrl` and `imageUrl` in ITEM lootbox results |
| `bot/modules/inventory/inventory.view.ts` | Modify | Replace `getItemUseResultEmbed()` with Components V2 `getLootboxResultMessage()`, remove local `defaultName` |
| `bot/commands/inventory/use.ts` | Modify | Switch from embed reply to Components V2 message |
| `bot/modules/economy/shop.view.ts` | Modify | Rework loot table into separate Container 2, replace local constants with shared imports, remove local `defaultName` |
---
### Task 1: Create shared rarity config
**Files:**
- Create: `shared/lib/rarity.ts`
- Create: `shared/lib/rarity.test.ts`
- [ ] **Step 1: Write the failing tests**
```typescript
// shared/lib/rarity.test.ts
import { describe, it, expect } from "bun:test";
import { getRarityConfig, RARITY_CONFIG, defaultName } from "./rarity";
describe("getRarityConfig", () => {
it("returns correct config for known rarities", () => {
expect(getRarityConfig("SSR").color).toBe(0xF1C40F);
expect(getRarityConfig("SSR").emoji).toBe("🌟");
expect(getRarityConfig("SSR").label).toBe("SSR");
});
it("returns correct config for loot types", () => {
expect(getRarityConfig("CURRENCY").color).toBe(0x2ECC71);
expect(getRarityConfig("XP").color).toBe(0x1ABC9C);
expect(getRarityConfig("NOTHING").color).toBe(0x636363);
});
it("falls back to Common for unknown rarity", () => {
const result = getRarityConfig("LEGENDARY");
expect(result).toEqual(RARITY_CONFIG["C"]);
});
});
describe("defaultName", () => {
it("extracts filename from path", () => {
expect(defaultName("/assets/items/sword.png")).toBe("sword.png");
});
it("returns image.png for empty path", () => {
expect(defaultName("")).toBe("image.png");
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `bun test shared/lib/rarity.test.ts`
Expected: FAIL — module not found
- [ ] **Step 3: Write the implementation**
```typescript
// shared/lib/rarity.ts
export const RARITY_CONFIG: Record<string, { color: number; emoji: string; label: string }> = {
C: { color: 0x95A5A6, emoji: "📦", label: "Common" },
R: { color: 0x3498DB, emoji: "📦", label: "Rare" },
SR: { color: 0x9B59B6, emoji: "✨", label: "Super Rare" },
SSR: { color: 0xF1C40F, emoji: "🌟", label: "SSR" },
CURRENCY: { color: 0x2ECC71, emoji: "💰", label: "Currency" },
XP: { color: 0x1ABC9C, emoji: "🔮", label: "Experience" },
NOTHING: { color: 0x636363, emoji: "💨", label: "Empty" },
};
export function getRarityConfig(rarity: string): { color: number; emoji: string; label: string } {
return RARITY_CONFIG[rarity] ?? RARITY_CONFIG["C"];
}
export function defaultName(path: string): string {
return path.split("/").pop() || "image.png";
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `bun test shared/lib/rarity.test.ts`
Expected: PASS — all 4 tests pass
- [ ] **Step 5: Commit**
```bash
git add shared/lib/rarity.ts shared/lib/rarity.test.ts
git commit -m "feat: add shared rarity config and helpers"
```
---
### Task 2: Update effect handler and pull result display (atomic change)
These changes are done together because the effect handler return shape change and the view layer consumer update must stay in sync — splitting them would leave a broken intermediate state.
**Files:**
- Modify: `shared/modules/inventory/effect.handlers.ts:141-153` (handleLootbox ITEM result)
- Modify: `bot/modules/inventory/inventory.view.ts` (replace `getItemUseResultEmbed` with `getLootboxResultMessage`)
- Modify: `bot/commands/inventory/use.ts:6,60-62` (update import and reply call)
- [ ] **Step 1: Update the ITEM result in `effect.handlers.ts` to return separate `iconUrl` and `imageUrl`**
In `shared/modules/inventory/effect.handlers.ts`, find the ITEM result return block (lines 141-153). Change the `item` object:
```typescript
// OLD (line 145-149)
item: {
name: item.name,
rarity: item.rarity,
description: item.description,
image: item.imageUrl || item.iconUrl
},
// NEW
item: {
name: item.name,
rarity: item.rarity,
description: item.description,
iconUrl: item.iconUrl,
imageUrl: item.imageUrl,
},
```
- [ ] **Step 2: Rewrite `inventory.view.ts` — replace `getItemUseResultEmbed` with `getLootboxResultMessage`**
Replace the entire `getItemUseResultEmbed` function (lines 37-136) and the local `defaultName` helper (lines 138-140). Update the imports at the top of the file. Keep `getInventoryEmbed` (lines 23-32) unchanged.
New imports (replace lines 1-6):
```typescript
import {
EmbedBuilder,
AttachmentBuilder,
ContainerBuilder,
SectionBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
ThumbnailBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
MessageFlags,
} from "discord.js";
import { resolveAssetUrl, isLocalAssetUrl } from "@shared/lib/assets";
import { getRarityConfig, defaultName } from "@shared/lib/rarity";
import { join } from "path";
import { existsSync } from "fs";
```
Note: `EmbedBuilder` is still needed because `getInventoryEmbed` uses it. Remove the `EffectType` import and the `ItemUsageData` type import since the new function doesn't use them.
New function (replaces `getItemUseResultEmbed` and `defaultName`):
```typescript
/**
* Creates a Components V2 message showing the result of opening a lootbox.
* Falls back to a simple embed for non-lootbox item usage.
*/
export function getLootboxResultMessage(
results: any[],
item?: { name: string; iconUrl: string | null; imageUrl: string | null; usageData: any }
) {
const files: AttachmentBuilder[] = [];
const otherMessages: string[] = [];
let lootResult: any = null;
for (const res of results) {
if (typeof res === "object" && res.type === "LOOTBOX_RESULT") {
lootResult = res;
} else {
otherMessages.push(typeof res === "string" ? `${res}` : `${JSON.stringify(res)}`);
}
}
// If no loot result, fall back to a simple embed (non-lootbox item usage)
if (!lootResult) {
const embed = new EmbedBuilder()
.setTitle(item ? `✅ Used ${item.name}` : "✅ Item Used!")
.setDescription(otherMessages.join("\n") || "Effect applied.")
.setColor(0x2ecc71)
.setTimestamp();
return { embeds: [embed], files, components: undefined, flags: undefined };
}
// Determine rarity key for theming
let rarityKey = "C";
if (lootResult.rewardType === "ITEM" && lootResult.item) {
rarityKey = lootResult.item.rarity || "C";
} else if (lootResult.rewardType === "CURRENCY") {
rarityKey = "CURRENCY";
} else if (lootResult.rewardType === "XP") {
rarityKey = "XP";
} else {
rarityKey = "NOTHING";
}
const config = getRarityConfig(rarityKey);
const container = new ContainerBuilder().setAccentColor(config.color);
// Header: lootbox name
if (item?.name) {
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`-# Opened: ${item.name}`)
);
}
// Build title and description based on reward type
let title = "";
let description = "";
if (lootResult.rewardType === "ITEM" && lootResult.item) {
const i = lootResult.item;
const amountStr = lootResult.amount > 1 ? ` ×${lootResult.amount}` : "";
title = `${config.emoji} ${config.label}${i.name}${amountStr}`;
description = i.description || "";
if (description) description += "\n";
description += `\n**${config.label}** · ×${lootResult.amount || 1} added to inventory`;
} else if (lootResult.rewardType === "CURRENCY") {
title = `${config.emoji} You found ${lootResult.amount.toLocaleString()} AU!`;
description = "Coins have been added to your balance.";
} else if (lootResult.rewardType === "XP") {
title = `${config.emoji} You gained ${lootResult.amount.toLocaleString()} XP!`;
description = "Experience has been added to your profile.";
} else {
title = `${config.emoji} Empty...`;
description = lootResult.message || "You found nothing inside.";
}
// Main section with optional thumbnail
const section = new SectionBuilder().addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# ${title}`),
new TextDisplayBuilder().setContent(description)
);
// Thumbnail from iconUrl (use reward item's icon for ITEM, lootbox icon otherwise)
let thumbnailUrl: string | null = null;
const iconSource = lootResult.rewardType === "ITEM" ? lootResult.item?.iconUrl : item?.iconUrl;
if (iconSource) {
if (isLocalAssetUrl(iconSource)) {
const iconPath = join(process.cwd(), "bot/assets/graphics", iconSource.replace(/^\/?assets\//, ""));
if (existsSync(iconPath)) {
const iconName = defaultName(iconSource);
files.push(new AttachmentBuilder(iconPath, { name: iconName }));
thumbnailUrl = `attachment://${iconName}`;
}
} else {
thumbnailUrl = resolveAssetUrl(iconSource);
}
}
if (thumbnailUrl) {
section.setThumbnailAccessory(new ThumbnailBuilder().setURL(thumbnailUrl));
}
container.addSectionComponents(section);
// Media gallery for full item art (if imageUrl differs from iconUrl)
if (lootResult.rewardType === "ITEM" && lootResult.item) {
const imgSource = lootResult.item.imageUrl;
const iconSrc = lootResult.item.iconUrl;
if (imgSource && imgSource !== iconSrc) {
let displayImageUrl: string | null = null;
if (isLocalAssetUrl(imgSource)) {
const imagePath = join(process.cwd(), "bot/assets/graphics", imgSource.replace(/^\/?assets\//, ""));
if (existsSync(imagePath)) {
const imageName = defaultName(imgSource);
if (!files.find(f => f.name === imageName)) {
files.push(new AttachmentBuilder(imagePath, { name: imageName }));
}
displayImageUrl = `attachment://${imageName}`;
}
} else {
displayImageUrl = resolveAssetUrl(imgSource);
}
if (displayImageUrl) {
container.addMediaGalleryComponents(
new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(displayImageUrl)
)
);
}
}
}
// Other effects (non-lootbox results like temp roles, XP boosts)
if (otherMessages.length > 0) {
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`**Other Effects**\n${otherMessages.join("\n")}`)
);
}
return {
components: [container] as any,
files,
flags: MessageFlags.IsComponentsV2,
embeds: undefined,
};
}
```
- [ ] **Step 3: Update `use.ts` to use the new function**
In `bot/commands/inventory/use.ts`:
Change the import (line 6):
```typescript
// OLD
import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view";
// NEW
import { getLootboxResultMessage } from "@/modules/inventory/inventory.view";
```
Change the reply (lines 60-62):
```typescript
// OLD
const { embed, files } = getItemUseResultEmbed(result.results, result.item);
await interaction.editReply({ embeds: [embed], files });
// NEW
const message = getLootboxResultMessage(result.results, result.item);
await interaction.editReply(message as any);
```
- [ ] **Step 4: Run the full test suite**
Run: `bun test`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add shared/modules/inventory/effect.handlers.ts bot/modules/inventory/inventory.view.ts bot/commands/inventory/use.ts
git commit -m "feat: rewrite lootbox pull results with Components V2 and separate icon/image URLs"
```
---
### Task 3: Rework shop loot table display
**Files:**
- Modify: `bot/modules/economy/shop.view.ts`
This task replaces the entire `getShopListingMessage` function body. The changes are:
1. Replace local `RarityColors`, `TitleMap`, and `defaultName` with shared imports
2. Split the loot table into a separate Container 2 with blurple accent
3. Group drops by rarity tier with aggregated percentages
4. Move purchase button conditionally (into loot table container for lootboxes, main container otherwise)
- [ ] **Step 1: Update imports**
At the top of `bot/modules/economy/shop.view.ts`:
Remove the local `RarityColors` constant (lines 24-32) and `TitleMap` constant (lines 34-42). Remove the local `defaultName` function at the bottom (lines 206-208).
Add import:
```typescript
import { getRarityConfig, defaultName } from "@shared/lib/rarity";
```
Keep existing imports for Discord.js builders, `resolveAssetUrl`, `isLocalAssetUrl`, `LootType`, `EffectType`, and `LootTableItem`.
- [ ] **Step 2: Rewrite the loot table section and purchase button placement**
Replace the loot table block (lines 122-184) and purchase button block (lines 186-195) with the new two-container logic. The key change is:
1. **Create `buyButton` before the conditional** (move lines 187-191 up, before line 122):
```typescript
const buyButton = new ButtonBuilder()
.setCustomId(`shop_buy_${item.id}`)
.setLabel(`Purchase for ${item.price} 🪙`)
.setStyle(ButtonStyle.Success)
.setEmoji("🛒");
```
2. **Replace lines 122-195** (old loot table + old unconditional button) with:
```typescript
if (item.usageData?.effects?.find((e: any) => e.type === EffectType.LOOTBOX)) {
const lootboxEffect = item.usageData.effects.find((e: any) => e.type === EffectType.LOOTBOX);
const pool = lootboxEffect.pool as LootTableItem[];
const totalWeight = pool.reduce((sum: number, i: LootTableItem) => sum + i.weight, 0);
const lootContainer = new ContainerBuilder().setAccentColor(0x5865F2);
lootContainer.addTextDisplayComponents(
new TextDisplayBuilder().setContent("## 🎁 Loot Table")
);
// Group drops by rarity tier
const tiers: Record<string, { items: string[]; totalChance: number }> = {};
for (const drop of pool) {
const chance = (drop.weight / totalWeight) * 100;
let line = "";
let rarity = "C";
switch (drop.type as any) {
case LootType.CURRENCY: {
const amt = (drop.minAmount != null && drop.maxAmount != null)
? `${drop.minAmount} ${drop.maxAmount}`
: (Array.isArray(drop.amount) ? `${drop.amount[0]} ${drop.amount[1]}` : `${drop.amount || 0}`);
line = `${amt} 🪙`;
rarity = "CURRENCY";
break;
}
case LootType.XP: {
const amt = (drop.minAmount != null && drop.maxAmount != null)
? `${drop.minAmount} ${drop.maxAmount}`
: (Array.isArray(drop.amount) ? `${drop.amount[0]} ${drop.amount[1]}` : `${drop.amount || 0}`);
line = `${amt} XP`;
rarity = "XP";
break;
}
case LootType.ITEM: {
const referencedItems = context?.referencedItems;
if (drop.itemId && referencedItems?.has(drop.itemId)) {
const i = referencedItems.get(drop.itemId)!;
line = `${i.name} ×${drop.amount || 1}`;
rarity = i.rarity;
} else {
line = `Unknown Item`;
rarity = "C";
}
break;
}
case LootType.NOTHING: {
line = "Nothing";
rarity = "NOTHING";
break;
}
}
if (line) {
if (!tiers[rarity]) tiers[rarity] = { items: [], totalChance: 0 };
tiers[rarity].items.push(line);
tiers[rarity].totalChance += chance;
}
}
const order = ["SSR", "SR", "R", "C", "CURRENCY", "XP", "NOTHING"];
let isFirst = true;
for (const rarity of order) {
const tier = tiers[rarity];
if (!tier || tier.items.length === 0) continue;
if (!isFirst) {
lootContainer.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
}
isFirst = false;
const config = getRarityConfig(rarity);
const chanceStr = tier.totalChance.toFixed(1);
lootContainer.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`${config.emoji} **${config.label}** — ${chanceStr}%`
),
new TextDisplayBuilder().setContent(tier.items.join(", "))
);
}
// Purchase button inside loot table container
lootContainer.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton)
);
containers.push(lootContainer);
} else {
// Non-lootbox items: purchase button stays in main container
mainContainer.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton)
);
}
```
3. **Update the main container accent color** (line 92): replace `RarityColors[item.rarity || "C"]` with `getRarityConfig(item.rarity || "C").color`.
- [ ] **Step 3: Run the full test suite**
Run: `bun test`
Expected: PASS
- [ ] **Step 4: Commit**
```bash
git add bot/modules/economy/shop.view.ts
git commit -m "feat: rework shop loot table into two-container Components V2 layout"
```
---
### Task 4: Manual verification checklist
- [ ] **Step 1: Start the bot**
Run: `bun --watch bot/index.ts`
- [ ] **Step 2: Test pull results in Discord**
Test each reward type by using a lootbox item:
- ITEM reward (verify accent color matches rarity, thumbnail shows, title format correct)
- CURRENCY reward (verify green accent, amount displayed)
- XP reward (verify aqua accent, amount displayed)
- NOTHING reward (verify gray accent, custom message shown)
- Item with both iconUrl and imageUrl (verify thumbnail + media gallery)
- Item without icon (no thumbnail, no crash)
- Lootbox with additional effects (verify "Other Effects" section appears)
- [ ] **Step 3: Test shop listing in Discord**
Use `/listing` command to post a lootbox item listing:
- Verify two containers appear (item info + loot table)
- Verify tiers are grouped with aggregated percentages
- Verify separators between tiers
- Verify purchase button is inside the loot table container
- Test with a non-lootbox item to verify purchase button stays in main container
- [ ] **Step 4: Test edge cases**
- Item without icon (no thumbnail, no crash)
- Item without image (no media gallery, no crash)
- Lootbox with only one tier
- Lootbox with all tiers populated

View File

@@ -1,874 +0,0 @@
# Inventory Display Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Redesign the `/inventory` command into a polished Components V2 experience with rarity emojis, paginated list, item detail view with artwork, and inline item actions (use/discard).
**Architecture:** Rewrite `inventory.view.ts` to produce CV2 containers for list and detail views. Rewrite `inventory.ts` command to manage pagination and view state with a collector. Add `inventory.interaction.ts` for interaction routing. Extend `RARITY_CONFIG` with square emojis.
**Tech Stack:** discord.js Components V2 (ContainerBuilder, TextDisplayBuilder, SectionBuilder, MediaGalleryBuilder, ActionRowBuilder, ButtonBuilder, StringSelectMenuBuilder), Drizzle ORM, Bun test runner.
---
## File Structure
| File | Action | Responsibility |
|------|--------|----------------|
| `shared/lib/rarity.ts` | Modify | Add `squareEmoji` field to `RARITY_CONFIG` |
| `bot/modules/inventory/inventory.view.ts` | Rewrite | CV2 list message builder, CV2 detail message builder (keep `getLootboxResultMessage` untouched) |
| `bot/modules/inventory/inventory.interaction.ts` | Create | Handle all inventory interactions (select, pagination, back, use, discard, confirm) |
| `bot/commands/inventory/inventory.ts` | Rewrite | Command definition with `view` subcommand, pagination collector, autocomplete |
---
### Task 1: Add squareEmoji to RARITY_CONFIG
**Files:**
- Modify: `shared/lib/rarity.ts`
- [ ] **Step 1: Update the RARITY_CONFIG type and entries**
In `shared/lib/rarity.ts`, update the type signature and add `squareEmoji` to each entry:
```typescript
/**
* Shared Rarity Configuration
* Provides the canonical rarity display config (colors, emoji, labels)
* used by lootbox pull results and shop loot table views.
*/
export const RARITY_CONFIG: Record<string, { color: number; emoji: string; squareEmoji: string; label: string }> = {
C: { color: 0x95A5A6, emoji: "📦", squareEmoji: "🟤", label: "Common" },
R: { color: 0x3498DB, emoji: "📦", squareEmoji: "🔵", label: "Rare" },
SR: { color: 0x9B59B6, emoji: "✨", squareEmoji: "🟣", label: "Super Rare" },
SSR: { color: 0xF1C40F, emoji: "🌟", squareEmoji: "🟡", label: "SSR" },
CURRENCY: { color: 0x2ECC71, emoji: "💰", squareEmoji: "💰", label: "Currency" },
XP: { color: 0x1ABC9C, emoji: "🔮", squareEmoji: "🔮", label: "Experience" },
NOTHING: { color: 0x636363, emoji: "💨", squareEmoji: "💨", label: "Empty" },
};
export function getRarityConfig(rarity: string): { color: number; emoji: string; squareEmoji: string; label: string } {
return RARITY_CONFIG[rarity] ?? (RARITY_CONFIG["C"]!);
}
```
- [ ] **Step 2: Verify nothing is broken**
Run: `bun test`
Expected: All existing tests pass (lootbox and other rarity consumers still work since they access `emoji`, not `squareEmoji`).
- [ ] **Step 3: Commit**
```bash
git add shared/lib/rarity.ts
git commit -m "feat(inventory): add squareEmoji to RARITY_CONFIG"
```
---
### Task 2: Build the inventory list view (CV2)
**Files:**
- Rewrite: `bot/modules/inventory/inventory.view.ts`
This task rewrites `getInventoryEmbed``getInventoryListMessage` and adds `getItemDetailMessage`. The existing `getLootboxResultMessage` function stays untouched.
- [ ] **Step 1: Define constants and types at the top of inventory.view.ts**
Replace the existing `InventoryEntry` interface and add constants. Keep all existing imports and add the new ones needed:
```typescript
import {
EmbedBuilder,
AttachmentBuilder,
ContainerBuilder,
SectionBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
ThumbnailBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
MessageFlags,
} from "discord.js";
import { resolveAssetUrl, isLocalAssetUrl } from "@shared/lib/assets";
import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity";
import { ItemType } from "@shared/lib/constants";
import type { ItemUsageData } from "@shared/lib/types";
import { join } from "path";
import { existsSync } from "fs";
export const ITEMS_PER_PAGE = 5;
const RARITY_SORT_ORDER: Record<string, number> = {
SSR: 0,
SR: 1,
R: 2,
C: 3,
};
export interface InventoryItem {
id: number;
name: string;
description: string | null;
rarity: string | null;
type: string;
price: bigint | null;
iconUrl: string;
imageUrl: string;
usageData: unknown;
}
export interface InventoryEntry {
quantity: bigint | null;
item: InventoryItem;
}
```
- [ ] **Step 2: Add the sortInventoryItems helper**
```typescript
export function sortInventoryItems(entries: InventoryEntry[]): InventoryEntry[] {
return [...entries].sort((a, b) => {
const rarityA = RARITY_SORT_ORDER[a.item.rarity ?? "C"] ?? 3;
const rarityB = RARITY_SORT_ORDER[b.item.rarity ?? "C"] ?? 3;
if (rarityA !== rarityB) return rarityA - rarityB;
return a.item.name.localeCompare(b.item.name);
});
}
```
- [ ] **Step 3: Implement getInventoryListMessage**
```typescript
export function getInventoryListMessage(
entries: InventoryEntry[],
username: string,
page: number,
viewerId: string,
ownerId: string,
) {
const sorted = sortInventoryItems(entries);
const totalPages = Math.max(1, Math.ceil(sorted.length / ITEMS_PER_PAGE));
const safePage = Math.min(page, totalPages - 1);
const pageItems = sorted.slice(safePage * ITEMS_PER_PAGE, (safePage + 1) * ITEMS_PER_PAGE);
// Accent color from highest-rarity item on page
const highestRarity = pageItems[0]?.item.rarity ?? "C";
const accentColor = getRarityConfig(highestRarity).color;
const container = new ContainerBuilder()
.setAccentColor(accentColor)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# 📦 ${username}'s Inventory`),
new TextDisplayBuilder().setContent(`-# ${sorted.length} item${sorted.length !== 1 ? "s" : ""} total`)
);
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Item rows
const lines = pageItems.map((entry) => {
const rc = getRarityConfig(entry.item.rarity ?? "C");
return `${rc.squareEmoji} **${entry.item.name}** — ${rc.label} · ${entry.item.type} · ×${entry.quantity}`;
});
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(lines.join("\n"))
);
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Select menu with current page items
const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`inv_select_${viewerId}`)
.setPlaceholder("Select an item for details");
for (const entry of pageItems) {
const rc = getRarityConfig(entry.item.rarity ?? "C");
selectMenu.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel(entry.item.name)
.setDescription(`${rc.label} · ${entry.item.type}`)
.setValue(entry.item.id.toString())
);
}
container.addActionRowComponents(
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
);
// Pagination buttons
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_prev_${viewerId}`)
.setLabel("◀ Previous")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage <= 0),
new ButtonBuilder()
.setCustomId(`inv_page_${viewerId}`)
.setLabel(`Page ${safePage + 1}/${totalPages}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId(`inv_next_${viewerId}`)
.setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage >= totalPages - 1),
);
container.addActionRowComponents(navRow);
return {
components: [container] as any,
files: [] as AttachmentBuilder[],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
```
- [ ] **Step 4: Implement getEmptyInventoryMessage**
```typescript
export function getEmptyInventoryMessage(username: string) {
const container = new ContainerBuilder()
.setAccentColor(0x95A5A6)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# 📦 ${username}'s Inventory`),
new TextDisplayBuilder().setContent("*No items yet. Visit the shop or complete quests to earn items!*")
);
return {
components: [container] as any,
files: [],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
```
- [ ] **Step 5: Implement getItemDetailMessage**
```typescript
export function getItemDetailMessage(
entry: InventoryEntry,
viewerId: string,
ownerId: string,
) {
const { item } = entry;
const rc = getRarityConfig(item.rarity ?? "C");
const files: AttachmentBuilder[] = [];
const container = new ContainerBuilder().setAccentColor(rc.color);
// Header section with thumbnail
const section = new SectionBuilder().addTextDisplayComponents(
new TextDisplayBuilder().setContent(`${rc.squareEmoji} **${item.name}**`),
new TextDisplayBuilder().setContent(`-# ${rc.label} · ${item.type}`)
);
// Resolve icon thumbnail
const iconUrl = resolveItemUrl(item.iconUrl, files);
if (iconUrl) {
section.setThumbnailAccessory(new ThumbnailBuilder().setURL(iconUrl));
}
container.addSectionComponents(section);
// Artwork via MediaGallery
const imageUrl = resolveItemUrl(item.imageUrl, files);
if (imageUrl && item.imageUrl !== item.iconUrl) {
container.addMediaGalleryComponents(
new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(imageUrl)
)
);
}
// Description
if (item.description) {
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(item.description)
);
}
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Stats row
const priceText = item.price ? `${item.price} 🪙` : "Not tradeable";
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Owned: **×${entry.quantity}** · Value: **${priceText}**`
)
);
// Action buttons
const isOwner = viewerId === ownerId;
const usageData = item.usageData as ItemUsageData | null;
const isUsable = isOwner && item.type === ItemType.CONSUMABLE &&
usageData?.effects && usageData.effects.length > 0;
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_back_${viewerId}`)
.setLabel("◀ Back")
.setStyle(ButtonStyle.Primary)
);
if (isUsable) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_use_${viewerId}`)
.setLabel("🧪 Use")
.setStyle(ButtonStyle.Success)
);
}
if (isOwner) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_${viewerId}`)
.setLabel("🗑 Discard")
.setStyle(ButtonStyle.Danger)
);
}
container.addActionRowComponents(actionRow);
return {
components: [container] as any,
files,
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
```
- [ ] **Step 6: Implement getDiscardConfirmMessage and the resolveItemUrl helper**
```typescript
export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string) {
const rc = getRarityConfig(entry.item.rarity ?? "C");
const container = new ContainerBuilder()
.setAccentColor(0xED4245)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Are you sure you want to discard 1× **${entry.item.name}**?`
)
)
.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_confirm_${viewerId}`)
.setLabel("Confirm")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`inv_discard_cancel_${viewerId}`)
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary)
)
);
return {
components: [container] as any,
files: [],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
/**
* Resolves an item URL (icon or image) for use in CV2 components.
* Handles both local assets and remote URLs.
* Pushes AttachmentBuilders to `files` array for local assets.
*/
function resolveItemUrl(url: string | null | undefined, files: AttachmentBuilder[]): string | null {
if (!url) return null;
if (isLocalAssetUrl(url)) {
const filePath = join(process.cwd(), "bot/assets/graphics", stripQuery(url).replace(/^\/?assets\//, ""));
if (existsSync(filePath)) {
const fileName = defaultName(url);
if (!files.find(f => f.name === fileName)) {
files.push(new AttachmentBuilder(filePath, { name: fileName }));
}
return `attachment://${fileName}`;
}
return null;
}
return resolveAssetUrl(url);
}
```
- [ ] **Step 7: Verify the file compiles**
Run: `bunx tsc --noEmit bot/modules/inventory/inventory.view.ts`
Expected: No type errors. (Note: `getLootboxResultMessage` remains unchanged below all the new code.)
- [ ] **Step 8: Commit**
```bash
git add bot/modules/inventory/inventory.view.ts
git commit -m "feat(inventory): rewrite inventory view with CV2 list and detail builders"
```
---
### Task 3: Create the inventory interaction handler
**Files:**
- Create: `bot/modules/inventory/inventory.interaction.ts`
- [ ] **Step 1: Create the interaction handler file**
```typescript
import type { StringSelectMenuInteraction, ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { getLootboxResultMessage } from "./inventory.view";
import type { ItemUsageData } from "@shared/lib/types";
import { getGuildConfig } from "@shared/lib/config";
export interface InventoryState {
ownerId: string;
viewerId: string;
page: number;
selectedItemId: number | null;
}
/**
* Extracts the viewer user ID from an inventory custom ID.
* Custom IDs follow the format: inv_{action}_{viewerId}
*/
export function parseInventoryCustomId(customId: string): { action: string; viewerId: string } | null {
const match = customId.match(/^inv_(\w+?)_(\d+)$/);
if (!match) return null;
return { action: match[1], viewerId: match[2] };
}
/**
* Checks if a custom ID belongs to the inventory system.
*/
export function isInventoryInteraction(customId: string): boolean {
return customId.startsWith("inv_");
}
/**
* Handles the "Use" button — executes item effects.
* Returns the result messages array from inventoryService.useItem,
* plus handles role-based effects that require the guild member.
*/
export async function executeItemUse(
interaction: ButtonInteraction,
userId: string,
itemId: number,
): Promise<{ results: any[]; usageData: ItemUsageData | null; item: any }> {
const result = await inventoryService.useItem(userId, itemId);
// Handle role effects (same logic as /use command)
const usageData = result.usageData;
if (usageData) {
const guildConfig = await getGuildConfig(interaction.guildId!);
const colorRoles = guildConfig.colorRoles ?? [];
for (const effect of usageData.effects) {
if (effect.type === "TEMP_ROLE" || effect.type === "COLOR_ROLE") {
try {
const member = await interaction.guild?.members.fetch(userId);
if (member) {
if (effect.type === "TEMP_ROLE") {
await member.roles.add(effect.roleId);
} else if (effect.type === "COLOR_ROLE") {
const rolesToRemove = colorRoles.filter((r: string) => member.roles.cache.has(r));
if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove);
await member.roles.add(effect.roleId);
}
}
} catch (e) {
console.error("Failed to assign role in inventory use:", e);
result.results.push("⚠️ Failed to assign role (Check bot permissions)");
}
}
}
}
return result;
}
```
- [ ] **Step 2: Verify the file compiles**
Run: `bunx tsc --noEmit bot/modules/inventory/inventory.interaction.ts`
Expected: No type errors.
- [ ] **Step 3: Commit**
```bash
git add bot/modules/inventory/inventory.interaction.ts
git commit -m "feat(inventory): add inventory interaction handler utilities"
```
---
### Task 4: Rewrite the inventory command
**Files:**
- Rewrite: `bot/commands/inventory/inventory.ts`
- [ ] **Step 1: Rewrite the command with subcommands, collector, and interaction routing**
```typescript
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags, ComponentType } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { createWarningEmbed, createErrorEmbed } from "@lib/embeds";
import {
getInventoryListMessage,
getEmptyInventoryMessage,
getItemDetailMessage,
getDiscardConfirmMessage,
sortInventoryItems,
ITEMS_PER_PAGE,
type InventoryEntry,
} from "@/modules/inventory/inventory.view";
import { getLootboxResultMessage } from "@/modules/inventory/inventory.view";
import {
parseInventoryCustomId,
isInventoryInteraction,
executeItemUse,
} from "@/modules/inventory/inventory.interaction";
import { UserError } from "@shared/lib/errors";
export const inventory = createCommand({
data: new SlashCommandBuilder()
.setName("inventory")
.setDescription("View your or another user's inventory")
.addSubcommand(sub =>
sub.setName("list")
.setDescription("View your or another user's inventory")
.addUserOption(option =>
option.setName("user")
.setDescription("User to view")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("view")
.setDescription("View details of a specific item")
.addNumberOption(option =>
option.setName("item")
.setDescription("The item to view")
.setRequired(true)
.setAutocomplete(true)
)
),
execute: async (interaction) => {
await interaction.deferReply();
const viewerId = interaction.user.id;
const subcommand = interaction.options.getSubcommand();
if (subcommand === "view") {
// Direct item detail view
const itemId = interaction.options.getNumber("item", true);
const user = await userService.getOrCreateUser(viewerId, interaction.user.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const entries = await inventoryService.getInventory(user.id.toString());
const entry = entries.find((e: any) => e.item.id === itemId);
if (!entry) {
await interaction.editReply({ embeds: [createWarningEmbed("Item not found in your inventory.", "Not Found")] });
return;
}
const ownerId = user.id.toString();
let currentPage = 0;
let selectedItemId: number | null = itemId;
const response = await interaction.editReply(
getItemDetailMessage(entry as InventoryEntry, viewerId, ownerId)
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
return;
}
// "list" subcommand
const targetUser = interaction.options.getUser("user") || interaction.user;
if (targetUser.bot) {
await interaction.editReply({ embeds: [createWarningEmbed("Bots do not have inventories.", "Inventory Check")] });
return;
}
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const ownerId = user.id.toString();
const entries = await inventoryService.getInventory(ownerId);
if (!entries || entries.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(user.username));
return;
}
let currentPage = 0;
let selectedItemId: number | null = null;
const response = await interaction.editReply(
getInventoryListMessage(entries as InventoryEntry[], user.username, currentPage, viewerId, ownerId)
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
},
autocomplete: async (interaction) => {
const focusedValue = interaction.options.getFocused();
const userId = interaction.user.id;
const results = await inventoryService.getAutocompleteItems(userId, focusedValue);
await interaction.respond(results);
},
});
async function setupCollector(
interaction: any,
response: any,
viewerId: string,
ownerId: string,
username: string,
initialPage: number,
initialItemId: number | null,
) {
let currentPage = initialPage;
let selectedItemId = initialItemId;
const collector = response.createMessageComponentCollector({
time: 120_000,
});
collector.on("collect", async (i: any) => {
if (i.user.id !== viewerId) return;
const parsed = parseInventoryCustomId(i.customId);
if (!parsed) return;
try {
await i.deferUpdate();
// Re-fetch inventory for fresh data
const entries = await inventoryService.getInventory(ownerId);
const sorted = sortInventoryItems(entries as InventoryEntry[]);
switch (parsed.action) {
case "select": {
const itemId = parseInt(i.values[0]);
const entry = sorted.find(e => e.item.id === itemId);
if (!entry) break;
selectedItemId = itemId;
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
break;
}
case "prev": {
currentPage = Math.max(0, currentPage - 1);
selectedItemId = null;
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
break;
}
case "next": {
currentPage = currentPage + 1;
selectedItemId = null;
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
break;
}
case "back": {
selectedItemId = null;
if (sorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
}
break;
}
case "use": {
if (viewerId !== ownerId || !selectedItemId) break;
try {
const result = await executeItemUse(i, viewerId, selectedItemId);
const message = getLootboxResultMessage(result.results, result.item);
await interaction.editReply(message as any);
// After showing result, wait briefly then return to detail or list
setTimeout(async () => {
try {
const freshEntries = await inventoryService.getInventory(ownerId);
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
if (freshEntry) {
await interaction.editReply(
getItemDetailMessage(freshEntry, viewerId, ownerId)
);
} else {
selectedItemId = null;
if (freshSorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
);
}
}
} catch {}
}, 3000);
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({
embeds: [createErrorEmbed(error.message)],
components: [],
flags: undefined,
});
} else {
throw error;
}
}
break;
}
case "discard": {
if (viewerId !== ownerId || !selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (!entry) break;
await interaction.editReply(
getDiscardConfirmMessage(entry, viewerId)
);
break;
}
case "discard_confirm": {
if (viewerId !== ownerId || !selectedItemId) break;
try {
await inventoryService.removeItem(ownerId, selectedItemId, 1n);
const freshEntries = await inventoryService.getInventory(ownerId);
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
if (freshEntry) {
await interaction.editReply(
getItemDetailMessage(freshEntry, viewerId, ownerId)
);
} else {
selectedItemId = null;
if (freshSorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
);
}
}
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({
embeds: [createErrorEmbed(error.message)],
components: [],
flags: undefined,
});
} else {
throw error;
}
}
break;
}
case "discard_cancel": {
if (!selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (!entry) break;
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
break;
}
}
} catch (error) {
console.error("Inventory interaction error:", error);
}
});
collector.on("end", () => {
interaction.editReply({ components: [] }).catch(() => {});
});
}
```
- [ ] **Step 2: Verify the file compiles**
Run: `bunx tsc --noEmit bot/commands/inventory/inventory.ts`
Expected: No type errors.
- [ ] **Step 3: Commit**
```bash
git add bot/commands/inventory/inventory.ts
git commit -m "feat(inventory): rewrite command with CV2 pagination and detail view"
```
---
### Task 5: Integration testing and verification
**Files:**
- All modified files
- [ ] **Step 1: Run the full test suite**
Run: `bun test`
Expected: All tests pass. The inventory service tests should still pass since we didn't change the service.
- [ ] **Step 2: Verify TypeScript compiles cleanly**
Run: `bunx tsc --noEmit`
Expected: No type errors across the entire project.
- [ ] **Step 3: Verify the bot starts**
Run: `bun --watch bot/index.ts` (start and verify no startup errors, then stop)
Expected: Bot initializes and registers commands without errors.
- [ ] **Step 4: Final commit if any fixes were needed**
```bash
git add -A
git commit -m "fix(inventory): address integration issues from inventory redesign"
```
(Only if fixes were needed in the previous steps.)

View File

@@ -1,122 +0,0 @@
# Lootbox UX Overhaul
**Date:** 2026-03-18
**Status:** Approved
## Problem
The current lootbox system has three UX issues:
1. **Pull results are visually flat** — a basic embed with plain text like "You found X!" with no visual differentiation between rarities.
2. **Shop loot table formatting is poor** — rewards are dumped as flat text lines grouped by rarity, with no visual hierarchy or scannability.
3. **No personality** — opening a lootbox feels like a database query response, not an event.
## Approach
**Full Components V2** — both pull results and shop loot tables use Discord's Components V2 system (containers, sections, media galleries, accent colors). No canvas image generation. Keeps the rendering approach consistent, simpler to build and maintain.
**Instant reveal** — no two-phase animations or button-driven reveals. The result appears immediately; excitement comes from visual quality and rarity theming.
**Loot table stays in shop only** — not shown in inventory or alongside pull results.
## Design: Pull Result
When a user opens a lootbox, the result is displayed as a Components V2 message (`flags: MessageFlags.IsComponentsV2`) with:
### Container
- **Accent color** driven by reward rarity:
- `C` (Common): `#95A5A6` (gray)
- `R` (Rare): `#3498DB` (blue)
- `SR` (Super Rare): `#9B59B6` (purple)
- `SSR`: `#F1C40F` (gold)
- `CURRENCY`: `#2ECC71` (green)
- `XP`: `#1ABC9C` (aqua)
- `NOTHING`: `#636363` (dark gray)
### Header
- Subtle context line: source lootbox name (e.g., "Opened: Astral Crate")
### Section (main content)
- **Title format (item rewards):** `🌟 SSR — Celestial Blade` (emoji + rarity + item name)
- **Title format (currency):** `💰 You found 1,250 AU!`
- **Title format (XP):** `🔮 You gained 500 XP!`
- **Title format (nothing):** `💨 Empty...`
- **Description:** Item description for items, contextual message for currency/XP/nothing. For NOTHING results, use the custom `lootResult.message` from the handler (falls back to "You found nothing inside.")
- **Rarity badge:** Shown as text below description for item rewards (e.g., "SSR" + "×1 added to inventory")
- **Thumbnail accessory:** Item icon (via `iconUrl`) when available
### Media Gallery
- If the item has an `imageUrl` different from `iconUrl`, display it in a media gallery below the section for full art showcase.
### Other Effects
- If the lootbox item has non-lootbox effects that also produce results (e.g., a lootbox that also grants XP or a temp role), display these as an additional text display below the main result: "**Other Effects**\n• Gained 100 XP\n• Temporary Role granted for 30m"
### Edge Cases
- **Unknown rarity:** If a reward item's rarity is not in `RARITY_CONFIG`, fall back to Common (`C`) styling.
- **Missing icon:** If no `iconUrl` is available, omit the thumbnail accessory entirely (section without accessory).
- **Missing image:** If no `imageUrl` is available (or same as `iconUrl`), omit the media gallery.
## Design: Shop Loot Table
When viewing a lootbox item in the shop, the listing uses two containers:
### Container 1: Item Info
- **Accent color:** Based on lootbox item's own rarity
- **Section:** Item name (heading), description, price
- **Thumbnail accessory:** Item icon
- **Media gallery:** Item image if different from icon
### Container 2: Loot Table + Purchase
- **Accent color:** Discord blurple (`#5865F2`)
- **Header:** `🎁 Loot Table`
- **Tiers listed in descending rarity order:** SSR → SR → R → C → Currency → XP → Nothing
- **Each tier shows:**
- Tier header: emoji + rarity label + aggregated chance percentage (sum of all items in that tier)
- Items listed inline, comma-separated (e.g., "Shadow Dagger ×1, Arcane Focus ×1")
- **Separators** between tiers for visual scannability
- **Tiers with no items are omitted**
- **Purchase button:** Action row inside this container with "🛒 Purchase for {price} 🪙" button (success style)
## Files to Modify
| File | Change |
|------|--------|
| `bot/modules/inventory/inventory.view.ts` | Replace `getItemUseResultEmbed()` with new Components V2 pull result builder |
| `bot/modules/economy/shop.view.ts` | Rework `getShopListingMessage()` loot table section into two-container layout |
| `bot/commands/inventory/use.ts` | Update to send Components V2 message with `flags: MessageFlags.IsComponentsV2` instead of embed |
| `shared/modules/inventory/effect.handlers.ts` | Modify `handleLootbox` ITEM result to return both `iconUrl` and `imageUrl` separately (currently collapses into single `image` field) |
## Shared Constants
The rarity color map and title/emoji map are currently duplicated between `shop.view.ts` and `inventory.view.ts`. Consolidate into a shared location (either a new `shared/lib/rarity.ts` or add to existing `shared/lib/constants.ts`).
Also consolidate the `defaultName` helper (duplicated in both view files) into a shared utility.
Rarity display config:
```typescript
const RARITY_CONFIG: Record<string, { color: number; emoji: string; label: string }> = {
C: { color: 0x95A5A6, emoji: "📦", label: "Common" },
R: { color: 0x3498DB, emoji: "📦", label: "Rare" },
SR: { color: 0x9B59B6, emoji: "✨", label: "Super Rare" },
SSR: { color: 0xF1C40F, emoji: "🌟", label: "SSR" },
CURRENCY: { color: 0x2ECC71, emoji: "💰", label: "Currency" },
XP: { color: 0x1ABC9C, emoji: "🔮", label: "Experience" },
NOTHING: { color: 0x636363, emoji: "💨", label: "Empty" },
};
```
## Out of Scope
- Loot table visibility in inventory or pull results
- Canvas-based image generation for pulls
- Two-phase or button-driven reveal mechanics
- Lootdrop system changes (channel activity drops are separate)
## Testing
- Existing lootbox tests should continue to pass (effect handler return shape changes are additive)
- Manual testing needed for visual output in Discord (Components V2 rendering)
- Verify all reward types render correctly: ITEM (all rarities), CURRENCY, XP, NOTHING
- Verify shop listing renders cleanly with various loot table sizes (1 tier, all tiers, many items per tier)
- Verify "other effects" display when lootbox item has multiple effect types
- Verify fallback behavior for items with unknown rarity, missing icons, missing images

View File

@@ -1,198 +0,0 @@
# Impersonate Panel — Design Spec
A Discohook-style webhook message editor inside the Aurora admin panel for sending messages as custom characters, with reusable presets.
## Summary of Decisions
| Decision | Choice |
|----------|--------|
| Channel targeting | Pick channel each time (dropdown) |
| Preset storage | PostgreSQL (new table) |
| Editor layout | Side-by-side (builder left, preview right) |
| Component adding | Drag & drop from palette |
| Preset management | Separate "Presets" tab with card grid |
| JSON editing | Bidirectional visual ↔ JSON toggle |
| Format support | Classic (content + embeds) AND Components V2 |
## Data Model
### New table: `webhook_presets`
| Column | Type | Description |
|--------|------|-------------|
| `id` | serial PK | Auto-increment ID |
| `name` | varchar(100) | Preset display name |
| `username` | varchar(80) | Webhook display name |
| `avatar_url` | text, nullable | Avatar image URL |
| `payload` | jsonb | Full webhook payload (content, embeds, components) |
| `created_by` | bigint | Discord user ID of creator |
| `created_at` | timestamp | Creation time |
| `updated_at` | timestamp | Last modified |
The `payload` column stores the complete webhook JSON body and is the source of truth. The visual editor reads/writes this JSONB directly.
**Notes:**
- `created_by` uses `bigint('created_by', { mode: 'bigint' })` with a foreign key reference to `users.id` (`onDelete: CASCADE`)
- `created_at` uses `.defaultNow()`
- `updated_at` is set by the application on every write (no database trigger)
- No `guild_id` scoping — Aurora is a single-guild bot
- The new schema file must be re-exported from `shared/db/schema/index.ts`
- Backend validation: reject payloads larger than 100KB
## API Endpoints
All endpoints are protected behind existing admin auth.
### Presets CRUD
- `GET /api/impersonate/presets` — list all presets
- `POST /api/impersonate/presets` — create preset
- `PUT /api/impersonate/presets/:id` — update preset
- `DELETE /api/impersonate/presets/:id` — delete preset
### Sending
- `POST /api/impersonate/send` — send webhook message to a channel
- Body: `{ channelId: string, username: string, avatarUrl?: string, payload: object }`
- **Bridge pattern:** The route handler imports `BotClient` to resolve the channel by ID (`client.channels.fetch(channelId)`) and obtain the client user. These discord.js objects are passed to the existing `sendWebhookMessage` utility from `bot/lib/webhookUtils.ts`. This is acceptable because `api/` already runs in the same Bun process as the bot.
### Channels
- `GET /api/impersonate/channels` — fetch guild text channels for the channel picker
- Returns `{ id, name, parentName }` grouped by category
## Frontend Architecture
### Page Structure
Two tabs at the top of the Impersonate page: **Editor** and **Presets**.
### Editor Tab (Side-by-Side)
**Left pane — Builder:**
- **Top bar:** Username input, avatar URL input, channel dropdown, format toggle (Classic / Components V2), JSON/Visual toggle
- **Component palette:** Draggable component types. Components V2: Text Display, Section, Media Gallery, Separator, Container, File, Action Row. Classic: Content, Embed
- **Message canvas:** Drop zone where components are arranged. Each dropped component expands into an inline collapsible form editor. Drag to reorder via `@dnd-kit/core`
- **Bottom bar:** "Send" button and "Save as Preset" button
**Right pane — Preview:**
- Discord-styled message preview (dark theme, `#313338` background)
- Avatar circle + username + timestamp header
- Live-renders the current payload on every change
- Visual approximation of Discord's rendering, not pixel-perfect
### JSON Mode
Toggling to JSON replaces the visual builder with a monospace code editor. Edits sync bidirectionally. Invalid JSON shows an inline error and blocks switching back to visual mode until fixed.
### Presets Tab
- Card grid of saved presets showing avatar, name, and truncated payload preview
- Click a card to load it into the editor tab
- Edit/delete actions on each card
### File Structure
```
panel/src/
├── pages/
│ ├── Impersonate.tsx # Main page, tab switching, top-level state
│ └── impersonate/
│ ├── Editor.tsx # Side-by-side builder + preview layout
│ ├── Preview.tsx # Discord-style message renderer
│ ├── Presets.tsx # Preset card grid
│ ├── ComponentPalette.tsx # Draggable component type list
│ └── components/
│ ├── TextDisplayEditor.tsx
│ ├── SectionEditor.tsx
│ ├── MediaGalleryEditor.tsx
│ ├── SeparatorEditor.tsx
│ ├── ContainerEditor.tsx
│ ├── FileEditor.tsx
│ ├── ActionRowEditor.tsx
│ ├── EmbedEditor.tsx # Classic mode
│ └── ContentEditor.tsx # Classic mode
├── lib/
│ └── useImpersonate.ts # API hook for presets CRUD + send + channels
```
Backend:
```
shared/db/schema/
│ └── webhook-presets.ts # New schema file (re-export from index.ts)
shared/modules/impersonate/
│ └── impersonate.service.ts # Preset CRUD + send logic
api/src/routes/
│ └── impersonate.routes.ts # Route handler (register in index.ts protectedRoutes)
```
### Panel Wiring
- Add `"impersonate"` to the `Page` union type in `Layout.tsx`
- Add nav item to `navItems` array in `Layout.tsx` with appropriate Lucide icon
- Add conditional render branch in `App.tsx`
**Note:** The `pages/impersonate/` sub-directory is a new pattern — existing pages are flat files. This is justified by the complexity of this feature (9+ component files). Flat pages remain appropriate for simpler pages.
## Component Editors
Each component type gets an inline collapsible editor card on the canvas.
### Components V2
| Component | Editable Fields |
|-----------|----------------|
| **Text Display** | Markdown content textarea |
| **Section** | Text content, accessory type (button or thumbnail), accessory config (URL, label, style) |
| **Media Gallery** | List of media items: URL, alt text, spoiler toggle. Add/remove items |
| **Separator** | Spacing size toggle (small/large) |
| **Container** | Accent color picker, nested drop zone (accepts Text Display, Section, Media Gallery, Separator, Action Row, File) |
| **File** | URL input, filename |
| **Action Row** | Buttons: label, style (Primary/Secondary/Success/Danger/Link), URL/custom ID, emoji, disabled toggle. Select menus: placeholder, options list, min/max values |
### Classic Mode
| Component | Editable Fields |
|-----------|----------------|
| **Content** | Markdown textarea |
| **Embed** | Title, description, URL, color picker, timestamp, author (name, icon URL), footer (text, icon URL), image URL, thumbnail URL, fields (array of name, value, inline toggle) |
### Webhook-Level Options
- `tts` toggle
- `thread_name` input (for forum channels)
- `flags` (suppress embeds/notifications)
- When Components V2 format is selected, the payload must include `flags: 32768` (`IS_COMPONENTS_V2` flag, `1 << 15`). This is set automatically by the editor when the format toggle is on Components V2.
## Preview Renderer
Renders a Discord-style message mock in the right pane:
- Dark background (`#313338`)
- Avatar circle + username + "Today at HH:MM" timestamp header
- Components V2: containers with accent-colored left border, text blocks with markdown rendering, media gallery as responsive image grid, buttons as pill-shaped elements with Discord color scheme, separators as horizontal rules
- Classic: content as rendered markdown, embeds with colored left border, field grids, inline images
- Live updates on every change
This is a visual approximation for authoring purposes, not a 1:1 Discord replica.
## Error Handling
| Scenario | Behavior |
|----------|----------|
| Invalid JSON on toggle | Show inline error, block switch to visual until fixed |
| Send failure | Display Discord API error message inline (e.g., "Missing permissions") |
| Empty payload | Disable Send button |
| Discord payload limits | Validate against limits (6000 char embeds, 10 components per action row, 5 action rows) and show warnings |
| Channel permission errors | Surface "Bot lacks MANAGE_WEBHOOKS permission" clearly |
| Invalid avatar URL | Lightweight `https://` check; Discord rejects bad URLs on send |
| Preset name collision | Allowed — presets identified by ID |
## Dependencies
- `@dnd-kit/core` + `@dnd-kit/sortable` — drag and drop
- No other new dependencies expected; existing stack (React, Tailwind, Lucide) covers the rest

View File

@@ -1,123 +0,0 @@
# Inventory Display Redesign
## Overview
Redesign the `/inventory` command from a basic embed listing to a polished Components V2 experience with rarity indicators, paginated list view, item detail view with artwork, and inline item management actions.
## Rarity Emoji Mapping
Add a `squareEmoji` field to `RARITY_CONFIG` in `shared/lib/rarity.ts`:
| Rarity | squareEmoji | Existing emoji | Color hex |
|--------|-------------|----------------|-----------|
| C | 🟤 | 📦 | 0x95A5A6 |
| R | 🔵 | 📦 | 0x3498DB |
| SR | 🟣 | ✨ | 0x9B59B6 |
| SSR | 🟡 | 🌟 | 0xF1C40F |
Non-item rarities (CURRENCY, XP, NOTHING) do not get square emojis. The existing `emoji` field remains unchanged (used by lootbox results).
## List View
The `/inventory [user]` command renders a Components V2 message:
1. **Header**`TextDisplayBuilder`: `# 📦 {username}'s Inventory` with subtitle showing total item count.
2. **Separator**
3. **Item rows** (5 per page) — Each item is a `TextDisplayBuilder` line: `{squareEmoji} **{Item Name}** — {Rarity Label} · {Type} · ×{quantity}`
4. **Separator**
5. **Select menu**`StringSelectMenuBuilder` populated with the 5 items on the current page. Placeholder: "Select an item for details". Each option shows item name and rarity label.
6. **Navigation row**`ActionRowBuilder`: `◀ Previous` (disabled on page 1), disabled `Page X/Y` indicator button, `Next ▶` (disabled on last page).
**Container:** `ContainerBuilder` with accent color from the highest-rarity item on the current page.
**Sorting:** Items sorted by rarity descending (SSR → SR → R → C), then alphabetically within the same rarity.
**Empty state:** If inventory is empty, show: "No items yet. Visit the shop or complete quests to earn items!"
**Collector:** `createMessageComponentCollector` with 2-minute idle timeout. On timeout, disable all interactive components.
## Detail View
Shown when a user selects an item from the dropdown or uses `/inventory view <item>`:
1. **Header section**`SectionBuilder`:
- `TextDisplayBuilder`: `{squareEmoji} **{Item Name}**` with subtitle `-# {Rarity Label} · {Type}`
- `ThumbnailBuilder` with the item's `iconUrl`
2. **Artwork**`MediaGalleryBuilder` displaying the item's `imageUrl`
3. **Description**`TextDisplayBuilder` with the item's `description`
4. **Separator**
5. **Stats row**`TextDisplayBuilder`: `Owned: **×{quantity}**` and `Value: **{price} 🪙**` (or "Not tradeable" if price is null)
6. **Action buttons**`ActionRowBuilder`:
- `◀ Back` (primary) — always shown, returns to list view at the same page
- `🧪 Use` (success) — only shown if **viewer is the owner** AND item type is CONSUMABLE with effects defined
- `🗑 Discard` (danger) — only shown if **viewer is the owner**
**Container:** `ContainerBuilder` with accent color matching the item's rarity color.
### Ownership Protection
The command tracks two IDs: `viewerId` (who ran the command) and `ownerId` (whose inventory is displayed). When `viewerId !== ownerId`, the inventory is **read-only**:
- The detail view only shows the Back button (no Use or Discard).
- The interaction handler validates `viewerId === ownerId` before executing `useItem` or `removeItem`, as a server-side guard even if the buttons were somehow rendered.
### Use Button Flow
Calls `inventoryService.useItem()` and shows the result inline. Then returns to the detail view with updated quantity. If quantity reaches 0, returns to the list view.
### Discard Flow
1. Clicking `🗑 Discard` replaces the action row with a confirmation: "Discard 1× {Item Name}?" with `Confirm` (danger) and `Cancel` (secondary) buttons.
2. On confirm: calls `inventoryService.removeItem(userId, itemId, 1)`, returns to detail view with updated quantity. If quantity reaches 0, returns to list view.
3. On cancel: returns to the normal detail view action buttons.
## `/inventory view <item>` Subcommand
Adds a `view` subcommand with a required `item` string option that has autocomplete. Autocomplete queries the user's inventory items (reusing the pattern from `getAutocompleteItems`). Goes directly to the detail view. The Back button returns to the full paginated list at page 1.
## Item Selection Entry Points
Two ways to reach the detail view:
- **Select menu dropdown** on the inventory list — for browsing
- **`/inventory view <item>`** subcommand — for direct access when the user knows the item name
Both render the same detail view.
## Interaction Custom IDs
All custom IDs include the invoking user's ID to prevent other users from interacting:
| Custom ID | Purpose |
|-----------|---------|
| `inv_select_{userId}` | Item select menu |
| `inv_prev_{userId}` | Previous page button |
| `inv_next_{userId}` | Next page button |
| `inv_back_{userId}` | Back to list from detail |
| `inv_use_{userId}` | Use item button |
| `inv_discard_{userId}` | Discard item button |
| `inv_discard_confirm_{userId}` | Confirm discard |
| `inv_discard_cancel_{userId}` | Cancel discard |
## File Changes
### Modified
- **`shared/lib/rarity.ts`** — Add `squareEmoji` field to `RARITY_CONFIG` entries for C, R, SR, SSR.
- **`bot/commands/inventory/inventory.ts`** — Rewrite to CV2 with pagination collector. Add `view` subcommand with autocomplete. Command setup and collector logic live here.
- **`bot/modules/inventory/inventory.view.ts`** — Replace `getInventoryEmbed` with `getInventoryListMessage` (builds the paginated CV2 list) and add `getItemDetailMessage` (builds the detail CV2 view). `getLootboxResultMessage` is untouched.
### New
- **`bot/modules/inventory/inventory.interaction.ts`** — Handles all inventory interaction routing: select menu item selection, pagination buttons, back navigation, use item, discard + confirmation flow.
### Unchanged
- `shared/modules/inventory/inventory.service.ts` — Already provides `getInventory`, `useItem`, `removeItem`, `getAutocompleteItems`.
- Database schema — All required fields (`iconUrl`, `imageUrl`, `description`, `rarity`, `type`, `price`) already exist on the items table.
## Pagination Details
- **Items per page:** 5
- **Page calculation:** `totalPages = Math.ceil(items.length / 5)`
- **Page clamping:** `safePage = Math.min(page, totalPages - 1)` to handle items being consumed while browsing
- **Collector timeout:** 2 minutes idle, matching the quest system pattern
- **On timeout:** Edit message to disable all buttons and the select menu

View File

@@ -28,8 +28,8 @@
"db:migrate-config": "docker compose run --rm app bun shared/scripts/migrate-config-to-db.ts",
"db:migrate-game-config": "docker compose run --rm app bun shared/scripts/migrate-game-settings-to-db.ts",
"db:migrate-all": "docker compose run --rm app sh -c 'bun shared/scripts/migrate-config-to-db.ts && bun shared/scripts/migrate-game-settings-to-db.ts'",
"test": "bash shared/scripts/test-sequential.sh",
"test:ci": "bash shared/scripts/test-sequential.sh --integration",
"test": "bash shared/scripts/test-isolated.sh",
"test:ci": "bash shared/scripts/test-isolated.sh --integration",
"test:simulate-ci": "bash shared/scripts/simulate-ci.sh",
"panel:dev": "cd panel && bun run dev",
"panel:build": "cd panel && bun run build",
@@ -40,10 +40,12 @@
},
"dependencies": {
"@napi-rs/canvas": "^0.1.89",
"chess.js": "^1.4.0",
"discord.js": "^14.25.1",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"mitt": "^3.0.1",
"postgres": "^3.4.8",
"zod": "^4.3.6"
}
}
}

87
panel/AGENTS.md Normal file
View File

@@ -0,0 +1,87 @@
# Panel
## Stack
- React 19
- React Router 7
- Vite 6
- Tailwind CSS v4 via `@tailwindcss/vite`
- Local utilities: `clsx`, `tailwind-merge`, `class-variance-authority`
- Icons: `lucide-react`
The panel lives in `panel/src` and is built to `panel/dist`.
## Dev and runtime
- `bun run panel:dev` starts Vite on `http://localhost:5173`
- Vite proxies `/api`, `/auth`, `/assets`, and `/ws` to `http://localhost:3000`
- The Bun server serves the built panel from `panel/dist` in integrated mode
## Auth model
- `useAuth()` calls `GET /auth/me`
- unauthenticated users are sent through `/auth/discord`
- logout uses `POST /auth/logout`
- non-enrolled users see the `NotEnrolled` page
Roles:
- `admin`: admin routes plus player/game routes
- `player`: player/game routes only
## Active routes
- `/dashboard`
- `/leaderboards`
- `/games`
- `/:gameSlug/:roomId`
- `/admin`
- `/admin/users`
- `/admin/items`
- `/admin/classes`
- `/admin/quests`
- `/admin/lootdrops`
- `/admin/moderation`
- `/admin/transactions`
- `/admin/settings`
## Data layer
Shared hooks in `panel/src/lib`:
- `useAuth`
- `useDashboard`
- `useUsers`
- `useItems`
- `useSettings`
- `useWebSocket`
- `useGameRoom`
`panel/src/lib/api.ts` is a thin fetch wrapper:
- base path is empty because the panel is usually same-origin with the Bun server
- 401 triggers a redirect back into `/auth/discord`
- 204 and empty responses return `undefined`
## WebSocket and games
- `useWebSocket()` keeps a singleton browser WebSocket connection
- reconnects with exponential backoff up to 30 seconds
- `useGameRoom()` multiplexes room traffic over that shared socket
- current built-in game UIs are chess and blackjack
## Styling
- Theme tokens live in `panel/src/index.css`
- Fonts currently loaded:
- Noto Serif
- Manrope
- Space Grotesk
- JetBrains Mono
- The implemented visual system is the "Stellar Editorial" direction documented in `docs/new-design/DESIGN.md`
## Current patterns
- Admin pages tend to use explicit `refetch()` after mutations instead of a shared cache layer
- Search inputs use a 300 ms debounce in hooks such as `useUsers()` and `useItems()`
- Layout and sidebar ownership lives in `panel/src/components/Layout.tsx`

View File

@@ -10,11 +10,14 @@
},
"dependencies": {
"@imgly/background-removal": "^1.7.0",
"chess.js": "^1.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.564.0",
"react": "^19.1.0",
"react-chessboard": "^5.10.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.13.2",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},

View File

@@ -0,0 +1,281 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="10_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/10_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 57.572834,25.099947 c 0,0 5.967372,-4.773898 5.967372,-11.392027 0,-3.8743954 -3.43972,-10.3065945 -11.392028,-10.3065945 -7.952308,0 -11.392028,6.4347116 -11.392028,10.3065945 0,6.618129 5.967373,11.392027 5.967373,11.392027 -6.62818,-5.163348 -18.444833,-1.638201 -18.444833,8.680956 0,5.16586 4.22113,10.849311 10.849311,10.849311 7.952308,0 11.392027,-8.680956 11.392027,-8.680956 0,0 1.010056,9.894531 -4.881939,15.191045 h 13.020178 c -5.891994,-5.294001 -4.881938,-15.191045 -4.881938,-15.191045 0,0 3.439718,8.680956 11.392027,8.680956 6.630693,0 10.849311,-5.685963 10.849311,-10.849311 0,-10.319157 -11.816654,-13.844304 -18.444833,-8.680956 z"
id="cl-9-8" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 57.110434,93.200747 c 0,0 5.967372,-4.773898 5.967372,-11.392027 0,-3.874396 -3.43972,-10.306594 -11.392028,-10.306594 -7.952308,0 -11.392028,6.434711 -11.392028,10.306594 0,6.618129 5.967373,11.392027 5.967373,11.392027 -6.62818,-5.163348 -18.444833,-1.638201 -18.444833,8.680953 0,5.16587 4.22113,10.84932 10.849311,10.84932 7.952308,0 11.392027,-8.68096 11.392027,-8.68096 0,0 1.010056,9.89453 -4.881939,15.19104 h 13.020178 c -5.891994,-5.294 -4.881938,-15.19104 -4.881938,-15.19104 0,0 3.439718,8.68096 11.392027,8.68096 6.630693,0 10.849311,-5.68597 10.849311,-10.84932 0,-10.319154 -11.816654,-13.844301 -18.444833,-8.680953 z"
id="cl-9-8-0" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 121.55789,24.926219 c 0,0 5.96737,-4.773898 5.96737,-11.392027 0,-3.8743954 -3.43971,-10.3065945 -11.39203,-10.3065945 -7.95231,0 -11.39202,6.4347116 -11.39202,10.3065945 0,6.618129 5.96737,11.392027 5.96737,11.392027 -6.62818,-5.163348 -18.444834,-1.638201 -18.444834,8.680956 0,5.16586 4.22113,10.849311 10.849304,10.849311 7.95231,0 11.39203,-8.680956 11.39203,-8.680956 0,0 1.01006,9.894531 -4.88193,15.191045 h 13.02017 c -5.89199,-5.294001 -4.88193,-15.191045 -4.88193,-15.191045 0,0 3.43971,8.680956 11.39202,8.680956 6.63069,0 10.84931,-5.685963 10.84931,-10.849311 0,-10.319157 -11.81665,-13.844304 -18.44483,-8.680956 z"
id="cl-9-8-9" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 121.55789,93.027019 c 0,0 5.96737,-4.773898 5.96737,-11.392028 0,-3.874395 -3.43971,-10.306593 -11.39203,-10.306593 -7.95231,0 -11.39202,6.434711 -11.39202,10.306593 0,6.61813 5.96737,11.392028 5.96737,11.392028 -6.62818,-5.163348 -18.444834,-1.638201 -18.444834,8.680951 0,5.16587 4.22113,10.84932 10.849304,10.84932 7.95231,0 11.39203,-8.68096 11.39203,-8.68096 0,0 1.01006,9.89453 -4.88193,15.19104 h 13.02017 c -5.89199,-5.294 -4.88193,-15.19104 -4.88193,-15.19104 0,0 3.43971,8.68096 11.39202,8.68096 6.63069,0 10.84931,-5.68597 10.84931,-10.84932 0,-10.319152 -11.81665,-13.844299 -18.44483,-8.680951 z"
id="cl-9-8-0-4" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 89.576798,59.281103 c 0,0 5.967372,-4.773897 5.967372,-11.392027 0,-3.874395 -3.43972,-10.306594 -11.392028,-10.306594 -7.952308,0 -11.392028,6.434712 -11.392028,10.306594 0,6.61813 5.967373,11.392027 5.967373,11.392027 -6.62818,-5.163347 -18.444833,-1.638201 -18.444833,8.680957 0,5.165859 4.22113,10.84931 10.849311,10.84931 7.952308,0 11.392027,-8.680956 11.392027,-8.680956 0,0 1.010056,9.894531 -4.881939,15.191045 h 13.020178 c -5.891994,-5.294001 -4.881938,-15.191045 -4.881938,-15.191045 0,0 3.439718,8.680956 11.392027,8.680956 6.63069,0 10.84931,-5.685963 10.84931,-10.84931 0,-10.319158 -11.816653,-13.844304 -18.444832,-8.680957 z"
id="cl-9-8-8" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 110.06258,217.80216 c 0,0 -5.96737,4.77391 -5.96737,11.39203 0,3.8744 3.43971,10.3066 11.39202,10.3066 7.95232,0 11.39203,-6.43471 11.39203,-10.3066 0,-6.61812 -5.96737,-11.39203 -5.96737,-11.39203 6.62818,5.16335 18.44483,1.6382 18.44483,-8.68095 0,-5.16586 -4.22112,-10.84931 -10.84931,-10.84931 -7.95231,0 -11.39202,8.68095 -11.39202,8.68095 0,0 -1.01006,-9.89453 4.88193,-15.19104 h -13.02017 c 5.89199,5.294 4.88193,15.19104 4.88193,15.19104 0,0 -3.43972,-8.68095 -11.39203,-8.68095 -6.630687,0 -10.849305,5.68596 -10.849305,10.84931 0,10.31915 11.816655,13.8443 18.444835,8.68095 z"
id="cl-9-8-4" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 110.70832,149.70136 c 0,0 -5.96737,4.77391 -5.96737,11.39203 0,3.8744 3.43971,10.3066 11.39202,10.3066 7.95232,0 11.39203,-6.43471 11.39203,-10.3066 0,-6.61812 -5.96737,-11.39203 -5.96737,-11.39203 6.62818,5.16335 18.44483,1.6382 18.44483,-8.68095 0,-5.16586 -4.22112,-10.84931 -10.84931,-10.84931 -7.95231,0 -11.39202,8.68095 -11.39202,8.68095 0,0 -1.01006,-9.89453 4.88193,-15.19104 h -13.02017 c 5.89199,5.294 4.88193,15.19104 4.88193,15.19104 0,0 -3.43972,-8.68095 -11.39203,-8.68095 -6.630687,0 -10.849305,5.68596 -10.849305,10.84931 0,10.31915 11.816655,13.8443 18.444835,8.68095 z"
id="cl-9-8-0-2" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 46.077633,217.97556 c 0,0 -5.967372,4.77391 -5.967372,11.39203 0,3.8744 3.43972,10.3066 11.392028,10.3066 7.952308,0 11.392028,-6.43471 11.392028,-10.3066 0,-6.61812 -5.967373,-11.39203 -5.967373,-11.39203 6.62818,5.16335 18.444833,1.6382 18.444833,-8.68095 0,-5.16586 -4.22113,-10.84931 -10.849311,-10.84931 -7.952308,0 -11.392027,8.68095 -11.392027,8.68095 0,0 -1.010056,-9.89453 4.881939,-15.19104 H 44.9922 c 5.891994,5.294 4.881938,15.19104 4.881938,15.19104 0,0 -3.439718,-8.68095 -11.392027,-8.68095 -6.630693,0 -10.849311,5.68596 -10.849311,10.84931 0,10.31915 11.816654,13.8443 18.444833,8.68095 z"
id="cl-9-8-9-6" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 46.261118,149.87509 c 0,0 -5.967372,4.77391 -5.967372,11.39203 0,3.8744 3.43972,10.3066 11.392028,10.3066 7.952308,0 11.392028,-6.43471 11.392028,-10.3066 0,-6.61812 -5.967373,-11.39203 -5.967373,-11.39203 6.62818,5.16335 18.444833,1.6382 18.444833,-8.68095 0,-5.16586 -4.22113,-10.84931 -10.849311,-10.84931 -7.952308,0 -11.392027,8.68095 -11.392027,8.68095 0,0 -1.010056,-9.89453 4.881939,-15.19104 H 45.175685 c 5.891994,5.294 4.881938,15.19104 4.881938,15.19104 0,0 -3.439718,-8.68095 -11.392027,-8.68095 -6.630693,0 -10.849311,5.68596 -10.849311,10.84931 0,10.31915 11.816654,13.8443 18.444833,8.68095 z"
id="cl-9-8-0-4-9" /><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-1.1621548"
y="27.170401"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="-1.1621548"
y="27.170401"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="11.000458"
y="27.499109"
id="text3038"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3040"
x="11.000458"
y="27.499109">0</tspan></text>
<path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 78.0698,183.9376 c 0,0 -5.96738,4.77389 -5.96738,11.39202 0,3.8744 3.43972,10.3066 11.39203,10.3066 7.95231,0 11.39203,-6.43471 11.39203,-10.3066 0,-6.61813 -5.96737,-11.39202 -5.96737,-11.39202 6.62818,5.16334 18.44483,1.6382 18.44483,-8.68096 0,-5.16586 -4.22113,-10.84931 -10.84931,-10.84931 -7.95231,0 -11.39203,8.68096 -11.39203,8.68096 0,0 -1.01005,-9.89454 4.88194,-15.19105 H 76.98436 c 5.892,5.294 4.88194,15.19105 4.88194,15.19105 0,0 -3.43972,-8.68096 -11.39203,-8.68096 -6.630688,0 -10.849308,5.68596 -10.849308,10.84931 0,10.31916 11.816658,13.8443 18.444838,8.68096 z"
id="cl-9-8-8-8" /><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-168.80901"
y="-216.22618"
id="text3788-0"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-168.80901"
y="-216.22618"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-156.64639"
y="-215.89748"
id="text3038-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3040-9"
x="-156.64639"
y="-215.89748">0</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,401 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="10_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/10_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="0.4075976"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="0.4075976"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,210.91474)"
id="layer1-2-6-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,31.619539)"
id="layer1-2-6-8-2-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,151.18274)"
id="layer1-2-6-8-2-8-1"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,91.351539)"
id="layer1-2-6-8-2-8-1-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.89593,210.91474)"
id="layer1-2-6-8-8-9"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8-4"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.89593,31.619552)"
id="layer1-2-6-8-2-4-9"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3-0"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.89593,151.18274)"
id="layer1-2-6-8-2-8-1-9"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4-1"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.89593,91.351542)"
id="layer1-2-6-8-2-8-1-4-7"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4-9-7"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,83.213394,61.828949)"
id="layer1-2-6-8-2-4-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3-5"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,83.008034,180.83805)"
id="layer1-2-6-8-2-4-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3-5-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="13.216442"
y="26.376137"
id="text3788-43"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790-1"
x="13.216442"
y="26.376137"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">0</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-166.43544"
y="-215.98416"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-9"
x="-166.43544"
y="-215.98416"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-153.62659"
y="-216.0213"
id="text3788-43-2"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-1-0"
x="-153.62659"
y="-216.0213"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">0</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,407 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="10_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/10_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-128.9357"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-0.98704022"
y="29.202564"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="-0.98704022"
y="29.202564"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,54.512268,30.003768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,54.512268,212.80617)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,54.512268,95.238623)"
id="layer1-9-6-8-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,54.512268,145.35601)"
id="layer1-9-6-8-884-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-3-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,111.99088,30.602538)"
id="layer1-9-6-8-7"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,111.99088,213.40494)"
id="layer1-9-6-8-9-7"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-2"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,111.99088,95.837397)"
id="layer1-9-6-8-8-7"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8-2"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,111.99088,145.95478)"
id="layer1-9-6-8-884-8-2"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-3-8-6"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.213391,62.704546)"
id="layer1-9-6-8-91"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-7"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,82.748515,179.70293)"
id="layer1-9-6-8-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="12.28669"
y="28.960051"
id="text3788-43"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790-1"
x="12.28669"
y="28.960051"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">0</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-167.59764"
y="-212.05972"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-5"
x="-167.59764"
y="-212.05972"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-154.32391"
y="-212.30225"
id="text3788-43-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-1-7"
x="-154.32391"
y="-212.30225"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">0</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="10_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/10_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="0.043596052"
y="28.342009"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="0.043596052"
y="28.342009"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929041,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,53.01089,31.765995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,53.339713,94.698498)"
id="layer1-7-88-8"><path
id="sl-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,53.789261,212.66394)"
id="layer1-7-88-4"><path
id="sl-4-3"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,53.460438,151.33144)"
id="layer1-7-88-8-1"><path
id="sl-4-8-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,114.97822,31.578035)"
id="layer1-7-88-9"><path
id="sl-4-2"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,115.30704,94.510535)"
id="layer1-7-88-8-0"><path
id="sl-4-8-6"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,115.75659,212.47597)"
id="layer1-7-88-4-8"><path
id="sl-4-3-9"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,115.42777,151.14347)"
id="layer1-7-88-8-1-2"><path
id="sl-4-8-4-6"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,84.152135,64.171198)"
id="layer1-7-88-6"><path
id="sl-4-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,84.480868,180.54025)"
id="layer1-7-88-6-6"><path
id="sl-4-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="12.206209"
y="28.670717"
id="text3038"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3040"
x="12.206209"
y="28.670717">0</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-166.83667"
y="-214.58258"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-166.83667"
y="-214.58258"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">1</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-154.67406"
y="-214.25388"
id="text3038-1"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3040-4"
x="-154.67406"
y="-214.25388">0</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="2_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/2_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-36.788386,-1.5311156)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,205.12954,245.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,318 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="2_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/2_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><filter
color-interpolation-filters="sRGB"
inkscape:collect="always"
id="filter3834-6-0"
x="-0.13934441"
width="1.2786888"
y="-0.16242018"
height="1.3248404"><feGaussianBlur
inkscape:collect="always"
stdDeviation="9.5105772"
id="feGaussianBlur3836-6-6" /></filter><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,82.928726,184.02194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,82.928726,55.619539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,308 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="2_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/2_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.701068,56.859768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,83.701068,185.26217)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="2_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/2_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929041,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,83.41089,57.365995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">2</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,83.45613,185.77132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,224 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="3_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/3_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-36.788386,-9.5311159)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,205.12954,253.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-36.788386,60.169684)"
id="layer1-1-4-8-2"><path
id="cl-9-8-0"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,319 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="3_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/3_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,82.928726,192.02194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,82.928726,50.819539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,82.928726,120.52034)"
id="layer1-2-6-8-2-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,318 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="3_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/3_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.312268,47.603768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,83.312268,192.00617)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.312268,118.90457)"
id="layer1-9-6-8-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="3_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/3_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929104,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,83.41089,49.365995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">3</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,83.45613,193.77132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,83.494697,119.06732)"
id="layer1-7-88-6"><path
id="sl-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="4_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/4_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-67.188386,-1.5311156)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,174.72954,245.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-9.1115857,-1.5311131)"
id="layer1-1-4-8-2"><path
id="cl-9-8-66"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,232.80634,245.27515)"
id="layer1-1-4-8-0-4"><path
id="cl-9-8-6-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,324 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="4_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/4_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.20553,184.02194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.20553,55.619539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,184.02194)"
id="layer1-2-6-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,55.619539)"
id="layer1-2-6-8-2-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,335 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="4_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/4_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,56.112268,64.859768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,56.112268,177.26217)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,110.70455,65.1323)"
id="layer1-9-6-8-0"
style="fill:#df0000;fill-opacity:1"
inkscape:export-filename="/home/byron/art/cards/final/layer1-9-6-8-9-8.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-6"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,110.70455,177.53469)"
id="layer1-9-6-8-9-8"
style="fill:#df0000;fill-opacity:1"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-9"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="4_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/4_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929041,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,54.61089,57.365995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">4</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,54.65613,185.77132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,112.688,57.35436)"
id="layer1-7-88-1"><path
id="sl-4-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,112.73324,185.75969)"
id="layer1-7-88-7-9"><path
id="sl-4-5-2"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="5_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/5_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-67.188386,-1.5311156)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,174.72954,245.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-9.1115857,-1.5311131)"
id="layer1-1-4-8-2"><path
id="cl-9-8-66"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,232.80634,245.27515)"
id="layer1-1-4-8-0-4"><path
id="cl-9-8-6-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-38.388386,61.769684)"
id="layer1-1-4-8-2-6"><path
id="cl-9-8-0"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,333 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="5_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/5_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.20553,184.02194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,112.20553,55.619539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,184.02194)"
id="layer1-2-6-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.128726,55.619539)"
id="layer1-2-6-8-2-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,82.283636,119.47398)"
id="layer1-2-6-8-2-4-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,336 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="5_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/5_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,56.112268,64.859768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,56.112268,177.26217)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,110.70455,65.1323)"
id="layer1-9-6-8-0"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-6"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,110.70455,177.53469)"
id="layer1-9-6-8-9-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-9"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.213395,125.05253)"
id="layer1-9-6-8-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="5_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/5_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929104,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,56.21089,49.365995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">5</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,56.25613,193.77132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,83.494697,119.06732)"
id="layer1-7-88-6"><path
id="sl-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,109.93095,49.495625)"
id="layer1-7-88-8"><path
id="sl-4-9"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,109.97619,193.90095)"
id="layer1-7-88-7-2"><path
id="sl-4-5-6"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="6_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/6_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-63.988386,-9.5311159)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,177.92954,253.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-63.988386,60.169684)"
id="layer1-1-4-8-2"><path
id="cl-9-8-0"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-11.20333,-9.7048439)"
id="layer1-1-4-8-8"><path
id="cl-9-8-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,230.7146,253.10142)"
id="layer1-1-4-8-0-2"><path
id="cl-9-8-6-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-11.20333,59.995956)"
id="layer1-1-4-8-2-6"><path
id="cl-9-8-0-4"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,340 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="6_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/6_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,110.12873,192.02194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,110.12873,50.819539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,110.12873,120.52034)"
id="layer1-2-6-8-2-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,56.013391,192.14005)"
id="layer1-2-6-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,56.013391,50.937663)"
id="layer1-2-6-8-2-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,56.013391,120.63845)"
id="layer1-2-6-8-2-8-1"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="6_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/6_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,57.712268,47.603768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,57.712268,192.00617)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,57.712268,118.90457)"
id="layer1-9-6-8-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,109.0504,47.272778)"
id="layer1-9-6-8-88"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-4"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,109.0504,191.67518)"
id="layer1-9-6-8-9-3"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,109.0504,118.57358)"
id="layer1-9-6-8-8-4"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8-9"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="6_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/6_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929104,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,56.21089,49.365995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">6</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,56.25613,193.77132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,56.294697,119.06732)"
id="layer1-7-88-6"><path
id="sl-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,110.89781,49.166905)"
id="layer1-7-88-68"><path
id="sl-4-84"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,110.94305,193.57223)"
id="layer1-7-88-7-3"><path
id="sl-4-5-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,110.98162,118.86823)"
id="layer1-7-88-6-4"><path
id="sl-4-8-9"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="7_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/7_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-63.988386,-27.131116)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,177.92954,269.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-63.988386,63.369684)"
id="layer1-1-4-8-2"><path
id="cl-9-8-0"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-11.20333,-27.304844)"
id="layer1-1-4-8-8"><path
id="cl-9-8-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,230.7146,269.10142)"
id="layer1-1-4-8-0-2"><path
id="cl-9-8-6-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-11.20333,63.195956)"
id="layer1-1-4-8-2-6"><path
id="cl-9-8-0-4"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-38.055702,18.622356)"
id="layer1-1-4-8-6"><path
id="cl-9-8-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,349 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="7_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/7_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,111.72873,193.62194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,111.72873,49.219539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,111.72873,120.52034)"
id="layer1-2-6-8-2-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.413391,193.74005)"
id="layer1-2-6-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.413391,49.337663)"
id="layer1-2-6-8-2-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.413391,120.63845)"
id="layer1-2-6-8-2-8-1"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,83.213393,85.53779)"
id="layer1-2-6-8-2-4-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="7_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/7_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,54.512268,31.603768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,54.512268,208.00617)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,54.512268,125.30457)"
id="layer1-9-6-8-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,112.2504,31.272778)"
id="layer1-9-6-8-88"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-4"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,112.2504,209.27518)"
id="layer1-9-6-8-9-3"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,112.2504,124.97358)"
id="layer1-9-6-8-8-4"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8-9"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.678269,76.705082)"
id="layer1-9-6-8-884"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-3"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,186 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="7_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/7_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929104,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,54.61089,44.565995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">7</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,54.65613,198.57132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,54.694697,119.06732)"
id="layer1-7-88-6"><path
id="sl-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,112.49781,44.366905)"
id="layer1-7-88-68"><path
id="sl-4-84"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,112.54305,198.37223)"
id="layer1-7-88-7-3"><path
id="sl-4-5-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,112.58162,118.86823)"
id="layer1-7-88-6-4"><path
id="sl-4-8-9"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,83.494697,83.937953)"
id="layer1-7-88-688"><path
id="sl-4-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="8_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/8_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-63.988386,-27.131116)"
id="layer1-1-4-8"><path
id="cl-9-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,177.92954,269.27515)"
id="layer1-1-4-8-0"><path
id="cl-9-8-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-63.988386,63.369684)"
id="layer1-1-4-8-2"><path
id="cl-9-8-0"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-11.20333,-27.304844)"
id="layer1-1-4-8-8"><path
id="cl-9-8-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,230.7146,269.10142)"
id="layer1-1-4-8-0-2"><path
id="cl-9-8-6-6"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-11.20333,63.195956)"
id="layer1-1-4-8-2-6"><path
id="cl-9-8-0-4"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(2.5125778,0,0,2.5125778,-38.055702,18.622356)"
id="layer1-1-4-8-6"><path
id="cl-9-8-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><g
transform="matrix(-2.5125778,0,0,-2.5125778,204.43127,226.5922)"
id="layer1-1-4-8-6-8"><path
id="cl-9-8-8-8"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,358 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="8_of_diamonds.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/8_of_diamonds.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><linearGradient
id="linearGradient2984"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#df0000;stop-opacity:0.64122134;"
offset="1"
id="stop2988" /></linearGradient><linearGradient
id="linearGradient3784-4-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8-8-2" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-1" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3100"
xlink:href="#linearGradient3784-4-4"
inkscape:collect="always" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.1224159,0.00551393,-0.00908973,-1.8503101,-0.0293938,-10.227695)"
cx="1.6632675e-13"
cy="-3.2337365"
fx="1.6632675e-13"
fy="-3.2337365"
r="8" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="72.124594"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="7.8456664"
y="26.413288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="7.8456664"
y="26.413288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-159.48785"
y="-216.71518"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-159.48785"
y="-216.71518"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(1.4769065,0,0,1.4769065,16.968095,44.236162)"
id="layer1-2-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(1.4769065,0,0,1.4769065,150.62089,198.50346)"
id="layer1-2-6-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-9"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,111.72873,193.62194)"
id="layer1-2-6-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,111.72873,49.219539)"
id="layer1-2-6-8-2"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,111.72873,120.52034)"
id="layer1-2-6-8-2-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.413391,193.74005)"
id="layer1-2-6-8-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.413391,49.337663)"
id="layer1-2-6-8-2-4"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,54.413391,120.63845)"
id="layer1-2-6-8-2-8-1"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-8-4"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,83.213393,85.53779)"
id="layer1-2-6-8-2-4-8"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-6-3-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g><g
transform="matrix(2.5882908,0,0,2.5882908,83.21339,156.92384)"
id="layer1-2-6-8-8-6"><path
style="fill:#df0000"
inkscape:connector-curvature="0"
id="dl-6-8-8-8"
d="M 3.2433274,-4.7253274 C 1.1263274,-7.5893274 0,-10.5 0,-10.5 c 0,0 -1.1263274,2.9106726 -3.2433274,5.7746726 C -5.3613274,-1.8623274 -8,0 -8,0 -8,0 -5.3613274,1.8613274 -3.2433274,4.7263274 -1.1263274,7.5893274 0,10.5 0,10.5 0,10.5 1.1263274,7.5893274 3.2433274,4.7263274 5.3613274,1.8613274 8,0 8,0 8,0 5.3613274,-1.8623274 3.2433274,-4.7253274 z"
sodipodi:nodetypes="ccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,364 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="8_of_hearts.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/8_of_hearts.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3781"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3773"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3775" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3777" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3773"
id="radialGradient3957"
cx="-0.15782039"
cy="-8.8345356"
fx="-0.15782039"
fy="-8.8345356"
r="7.9997029"
gradientTransform="matrix(-1.5842693,-0.02349808,0.03071979,-2.4775745,-0.24856378,-26.713507)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3959"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3961" /><stop
style="stop-color:#000000;stop-opacity:0.64885497;"
offset="1"
id="stop3963" /></linearGradient><radialGradient
r="81.902771"
fy="509.47577"
fx="168.02475"
cy="509.47577"
cx="168.02475"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
gradientUnits="userSpaceOnUse"
id="radialGradient3975"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.4351145;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-5"
id="radialGradient3929"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-5"><stop
style="stop-color:#ffffff;stop-opacity:0.48854962;"
offset="0"
id="stop3786-8-0" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-3" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784-4-1"
id="radialGradient3927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2565605,-0.77740644,0.33663816,0.5361257,-221.20213,359.24256)"
cx="168.02475"
cy="509.47577"
fx="168.02475"
fy="509.47577"
r="81.902771" /><linearGradient
id="linearGradient3784-4-1"><stop
style="stop-color:#ffffff;stop-opacity:0.23664123;"
offset="0"
id="stop3786-8-03" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-6" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3768"
id="radialGradient3776"
cx="-0.20602037"
cy="-4.5786963"
fx="-0.20602037"
fy="-4.5786963"
r="8"
gradientTransform="matrix(-1,0,0,-1.7201755,-0.41204074,-13.027194)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3768"><stop
style="stop-color:#df0000;stop-opacity:1;"
offset="0"
id="stop3770" /><stop
style="stop-color:#df0000;stop-opacity:0.67175573;"
offset="1"
id="stop3772" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013"
xlink:href="#linearGradient3784-4-6"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-6"><stop
style="stop-color:#ffffff;stop-opacity:0.31297711;"
offset="0"
id="stop3786-8-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-8" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient4013-8"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4-2"><stop
style="stop-color:#ffffff;stop-opacity:0.29007635;"
offset="0"
id="stop3786-8-1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6-5" /></linearGradient><radialGradient
r="81.902771"
fy="492.63205"
fx="159.35434"
cy="492.63205"
cx="159.35434"
gradientTransform="matrix(1.0894779,-0.71513803,0.44645273,0.65626582,-244.93331,290.9185)"
gradientUnits="userSpaceOnUse"
id="radialGradient3073"
xlink:href="#linearGradient3784-4-2"
inkscape:collect="always" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="1.7208768"
inkscape:cx="-28.405554"
inkscape:cy="147.27218"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.775425"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.775425"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(1.6743072,0,0,1.5669921,17.177511,46.385321)"
id="layer1-9-6"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#df0000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.81761"
y="-213.51517"
id="text3788-4"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-3"
x="-158.81761"
y="-213.51517"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#df0000;fill-opacity:1;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(-1.6743072,0,0,-1.5669921,150.15601,195.14313)"
id="layer1-9-6-5"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,54.512268,31.603768)"
id="layer1-9-6-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,54.512268,208.00617)"
id="layer1-9-6-8-9"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,54.512268,125.30457)"
id="layer1-9-6-8-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,112.2504,31.272778)"
id="layer1-9-6-8-88"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-4"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,112.2504,209.27518)"
id="layer1-9-6-8-9-3"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-5-1"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,112.2504,124.97358)"
id="layer1-9-6-8-8-4"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-8-9"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(2.7790082,0,0,2.600887,83.678269,76.705082)"
id="layer1-9-6-8-884"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-3"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g><g
transform="matrix(-2.7790082,0,0,-2.600887,83.213395,162.70775)"
id="layer1-9-6-8-884-8"
style="fill:#df0000;fill-opacity:1"><path
style="fill:#df0000;fill-opacity:1"
inkscape:connector-curvature="0"
id="hl-8-8-3-8"
d="M 3.676,-9 C 0.433,-9 0,-5.523 0,-5.523 0,-5.523 -0.433,-9 -3.676,-9 -5.946,-9 -8,-7.441 -8,-4.5 -8,-0.614 -1.4208493,3.2938141 0,9 1.35201,3.2985969 8,-0.614 8,-4.5 8,-7.441 5.946,-9 3.676,-9 z"
sodipodi:nodetypes="scsscss" /></g></svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="8_of_spades.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/8_of_spades.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="106.02254"
inkscape:cy="157.08206"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.5467014"
y="28.013288"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.5467014"
y="28.013288"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(1.5085945,0,0,1.3793253,16.929104,45.065897)"
id="layer1-7"><path
id="sl"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g>
<g
transform="matrix(2.6486789,0,0,2.4217176,54.61089,44.565995)"
id="layer1-7-88"><path
id="sl-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.97775"
y="-215.12402"
id="text3788-7"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-6"
x="-158.97775"
y="-215.12402"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">8</tspan></text>
<g
transform="matrix(-1.5085945,0,0,-1.3793253,150.22511,198.04408)"
id="layer1-7-3"><path
id="sl-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,54.65613,198.57132)"
id="layer1-7-88-7"><path
id="sl-4-5"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,54.694697,119.06732)"
id="layer1-7-88-6"><path
id="sl-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,112.49781,44.366905)"
id="layer1-7-88-68"><path
id="sl-4-84"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,112.54305,198.37223)"
id="layer1-7-88-7-3"><path
id="sl-4-5-1"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,112.58162,118.86823)"
id="layer1-7-88-6-4"><path
id="sl-4-8-9"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(2.6486789,0,0,2.4217176,83.494697,83.937953)"
id="layer1-7-88-688"><path
id="sl-4-4"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g><g
transform="matrix(-2.6486789,0,0,-2.4217176,83.165993,161.80325)"
id="layer1-7-88-688-6"><path
id="sl-4-4-8"
d="M 7.989,3.103 C 7.747,-0.954 0.242,-8.59 0,-10.5 c -0.242,1.909 -7.747,9.545 -7.989,13.603 -0.169,2.868 1.695,4.057 3.39,4.057 1.8351685,-0.021581 3.3508701,-2.8006944 3.873,-3.341 0.242,0.716 -1.603,6.682 -2.179,6.682 l 5.811,0 C 2.33,10.501 0.485,4.535 0.727,3.819 1.1841472,4.3152961 2.5241276,7.0768295 4.601,7.16 6.295,7.159 8.158,5.971 7.989,3.103 z"
inkscape:connector-curvature="0"
style="fill:#000000"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- http://code.google.com/p/vector-playing-cards/ -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="167.0869141pt"
height="242.6669922pt"
viewBox="0 0 167.0869141 242.6669922"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="0.48.0 r9654"
sodipodi:docname="9_of_clubs.svg"
inkscape:export-filename="/home/byron/art/cards/final/PNGs/9_of_clubs.png"
inkscape:export-xdpi="215.44792"
inkscape:export-ydpi="215.44792"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs41"><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2984"
id="radialGradient3760"
cx="48.231091"
cy="18.137882"
fx="48.231091"
fy="18.137882"
r="9.5"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.5605256,0.01828294,-0.02684055,-2.2909528,123.98377,58.809108)" /><linearGradient
id="linearGradient2984"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2986" /><stop
style="stop-color:#000000;stop-opacity:0.65648854;"
offset="1"
id="stop2988" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3784"
id="radialGradient3792"
cx="171.48665"
cy="511.22299"
fx="171.48665"
fy="511.22299"
r="81.902771"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse" /><linearGradient
id="linearGradient3784"><stop
style="stop-color:#ffffff;stop-opacity:0.53435117;"
offset="0"
id="stop3786" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788" /></linearGradient><radialGradient
r="81.902771"
fy="511.22299"
fx="171.48665"
cy="511.22299"
cx="171.48665"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3855"
xlink:href="#linearGradient3784-4"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-4"><stop
style="stop-color:#ffffff;stop-opacity:0.51908398;"
offset="0"
id="stop3786-8" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-6" /></linearGradient><radialGradient
r="81.902771"
fy="461.84113"
fx="181.69392"
cy="461.84113"
cx="181.69392"
gradientTransform="matrix(1.1529891,-0.67391547,0.39482025,0.67549043,-233.63262,270.40076)"
gradientUnits="userSpaceOnUse"
id="radialGradient3916"
xlink:href="#linearGradient3784-3"
inkscape:collect="always" /><linearGradient
id="linearGradient3784-3"><stop
style="stop-color:#ffffff;stop-opacity:0.70229006;"
offset="0"
id="stop3786-86" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3788-2" /></linearGradient></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="977"
id="namedview39"
showgrid="false"
inkscape:zoom="2.4336873"
inkscape:cx="117.62976"
inkscape:cy="148.16686"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="Layer_x0020_1"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<path
style="fill:#FFFFFF;stroke-width:0.5;"
d="M166.8369141,235.5478516c0,3.7773438-3.0869141,6.8691406-6.8710938,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805 c3.7841797,0,6.8710938,3.0927734,6.8710938,6.8701172v228.4277344z"
id="path5" />
<g
style="stroke:none;"
id="g7">
<g
id="g9">
</g>
</g>
<g
id="g15">
</g>
<g
id="g19">
</g>
<g
style="stroke:none;"
id="g23">
<g
id="g25">
</g>
</g>
<g
style="stroke:none;"
id="g31">
<g
id="g33">
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="8.3105459"
y="27.548409"
id="text3788"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3790"
x="8.3105459"
y="27.548409"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">9</tspan></text>
<g
transform="matrix(1.4856506,0,0,1.4856506,-54.024661,10.018072)"
id="layer1-1-4"><path
id="cl-9"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><text
xml:space="preserve"
style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
x="-158.86395"
y="-214.4666"
id="text3788-8"
sodipodi:linespacing="125%"
transform="scale(-1,-1)"><tspan
sodipodi:role="line"
id="tspan3790-7"
x="-158.86395"
y="-214.4666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">9</tspan></text>
<g
transform="matrix(-1.4856506,0,0,-1.4856506,221.19916,232.46182)"
id="layer1-1-4-1"><path
id="cl-9-7"
d="m 50.291466,22.698228 c 0,0 2.375,-1.9 2.375,-4.534 0,-1.542 -1.369,-4.102 -4.534,-4.102 -3.165,0 -4.534,2.561 -4.534,4.102 0,2.634 2.375,4.534 2.375,4.534 -2.638,-2.055 -7.341,-0.652 -7.341,3.455 0,2.056 1.68,4.318 4.318,4.318 3.165,0 4.534,-3.455 4.534,-3.455 0,0 0.402,3.938 -1.943,6.046 h 5.182 c -2.345,-2.107 -1.943,-6.046 -1.943,-6.046 0,0 1.369,3.455 4.534,3.455 2.639,0 4.318,-2.263 4.318,-4.318 0,-4.107 -4.703,-5.51 -7.341,-3.455 z"
inkscape:connector-curvature="0"
style="fill:#000000" /></g><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 57.572834,25.099947 c 0,0 5.967372,-4.773898 5.967372,-11.392027 0,-3.8743954 -3.43972,-10.3065945 -11.392028,-10.3065945 -7.952308,0 -11.392028,6.4347116 -11.392028,10.3065945 0,6.618129 5.967373,11.392027 5.967373,11.392027 -6.62818,-5.163348 -18.444833,-1.638201 -18.444833,8.680956 0,5.16586 4.22113,10.849311 10.849311,10.849311 7.952308,0 11.392027,-8.680956 11.392027,-8.680956 0,0 1.010056,9.894531 -4.881939,15.191045 h 13.020178 c -5.891994,-5.294001 -4.881938,-15.191045 -4.881938,-15.191045 0,0 3.439718,8.680956 11.392027,8.680956 6.630693,0 10.849311,-5.685963 10.849311,-10.849311 0,-10.319157 -11.816654,-13.844304 -18.444833,-8.680956 z"
id="cl-9-8" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 57.110434,93.200747 c 0,0 5.967372,-4.773898 5.967372,-11.392027 0,-3.874396 -3.43972,-10.306594 -11.392028,-10.306594 -7.952308,0 -11.392028,6.434711 -11.392028,10.306594 0,6.618129 5.967373,11.392027 5.967373,11.392027 -6.62818,-5.163348 -18.444833,-1.638201 -18.444833,8.680953 0,5.16587 4.22113,10.84932 10.849311,10.84932 7.952308,0 11.392027,-8.68096 11.392027,-8.68096 0,0 1.010056,9.89453 -4.881939,15.19104 h 13.020178 c -5.891994,-5.294 -4.881938,-15.19104 -4.881938,-15.19104 0,0 3.439718,8.68096 11.392027,8.68096 6.630693,0 10.849311,-5.68597 10.849311,-10.84932 0,-10.319154 -11.816654,-13.844301 -18.444833,-8.680953 z"
id="cl-9-8-0" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 121.55789,24.926219 c 0,0 5.96737,-4.773898 5.96737,-11.392027 0,-3.8743954 -3.43971,-10.3065945 -11.39203,-10.3065945 -7.95231,0 -11.39202,6.4347116 -11.39202,10.3065945 0,6.618129 5.96737,11.392027 5.96737,11.392027 -6.62818,-5.163348 -18.444834,-1.638201 -18.444834,8.680956 0,5.16586 4.22113,10.849311 10.849304,10.849311 7.95231,0 11.39203,-8.680956 11.39203,-8.680956 0,0 1.01006,9.894531 -4.88193,15.191045 h 13.02017 c -5.89199,-5.294001 -4.88193,-15.191045 -4.88193,-15.191045 0,0 3.43971,8.680956 11.39202,8.680956 6.63069,0 10.84931,-5.685963 10.84931,-10.849311 0,-10.319157 -11.81665,-13.844304 -18.44483,-8.680956 z"
id="cl-9-8-9" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 121.55789,93.027019 c 0,0 5.96737,-4.773898 5.96737,-11.392028 0,-3.874395 -3.43971,-10.306593 -11.39203,-10.306593 -7.95231,0 -11.39202,6.434711 -11.39202,10.306593 0,6.61813 5.96737,11.392028 5.96737,11.392028 -6.62818,-5.163348 -18.444834,-1.638201 -18.444834,8.680951 0,5.16587 4.22113,10.84932 10.849304,10.84932 7.95231,0 11.39203,-8.68096 11.39203,-8.68096 0,0 1.01006,9.89453 -4.88193,15.19104 h 13.02017 c -5.89199,-5.294 -4.88193,-15.19104 -4.88193,-15.19104 0,0 3.43971,8.68096 11.39202,8.68096 6.63069,0 10.84931,-5.68597 10.84931,-10.84932 0,-10.319152 -11.81665,-13.844299 -18.44483,-8.680951 z"
id="cl-9-8-0-4" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 89.576544,59.281103 c 0,0 5.967372,-4.773897 5.967372,-11.392027 0,-3.874395 -3.43972,-10.306594 -11.392028,-10.306594 -7.952308,0 -11.392028,6.434712 -11.392028,10.306594 0,6.61813 5.967373,11.392027 5.967373,11.392027 C 72.099053,54.117756 60.2824,57.642902 60.2824,67.96206 c 0,5.165859 4.22113,10.84931 10.849311,10.84931 7.952308,0 11.392027,-8.680956 11.392027,-8.680956 0,0 1.010056,9.894531 -4.881939,15.191045 h 13.020178 c -5.891994,-5.294001 -4.881938,-15.191045 -4.881938,-15.191045 0,0 3.439718,8.680956 11.392027,8.680956 6.630694,0 10.849314,-5.685963 10.849314,-10.84931 0,-10.319158 -11.816657,-13.844304 -18.444836,-8.680957 z"
id="cl-9-8-8" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 110.06258,217.80216 c 0,0 -5.96737,4.77391 -5.96737,11.39203 0,3.8744 3.43971,10.3066 11.39202,10.3066 7.95232,0 11.39203,-6.43471 11.39203,-10.3066 0,-6.61812 -5.96737,-11.39203 -5.96737,-11.39203 6.62818,5.16335 18.44483,1.6382 18.44483,-8.68095 0,-5.16586 -4.22112,-10.84931 -10.84931,-10.84931 -7.95231,0 -11.39202,8.68095 -11.39202,8.68095 0,0 -1.01006,-9.89453 4.88193,-15.19104 h -13.02017 c 5.89199,5.294 4.88193,15.19104 4.88193,15.19104 0,0 -3.43972,-8.68095 -11.39203,-8.68095 -6.630687,0 -10.849305,5.68596 -10.849305,10.84931 0,10.31915 11.816655,13.8443 18.444835,8.68095 z"
id="cl-9-8-4" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 110.70832,149.70136 c 0,0 -5.96737,4.77391 -5.96737,11.39203 0,3.8744 3.43971,10.3066 11.39202,10.3066 7.95232,0 11.39203,-6.43471 11.39203,-10.3066 0,-6.61812 -5.96737,-11.39203 -5.96737,-11.39203 6.62818,5.16335 18.44483,1.6382 18.44483,-8.68095 0,-5.16586 -4.22112,-10.84931 -10.84931,-10.84931 -7.95231,0 -11.39202,8.68095 -11.39202,8.68095 0,0 -1.01006,-9.89453 4.88193,-15.19104 h -13.02017 c 5.89199,5.294 4.88193,15.19104 4.88193,15.19104 0,0 -3.43972,-8.68095 -11.39203,-8.68095 -6.630687,0 -10.849305,5.68596 -10.849305,10.84931 0,10.31915 11.816655,13.8443 18.444835,8.68095 z"
id="cl-9-8-0-2" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 46.077528,217.97589 c 0,0 -5.967372,4.77391 -5.967372,11.39203 0,3.8744 3.43972,10.3066 11.392028,10.3066 7.952308,0 11.392028,-6.43471 11.392028,-10.3066 0,-6.61812 -5.967373,-11.39203 -5.967373,-11.39203 6.62818,5.16335 18.444833,1.6382 18.444833,-8.68095 0,-5.16586 -4.22113,-10.84931 -10.849311,-10.84931 -7.952308,0 -11.392027,8.68095 -11.392027,8.68095 0,0 -1.010056,-9.89453 4.881939,-15.19104 H 44.992095 c 5.891994,5.294 4.881938,15.19104 4.881938,15.19104 0,0 -3.439718,-8.68095 -11.392027,-8.68095 -6.630693,0 -10.849311,5.68596 -10.849311,10.84931 0,10.31915 11.816654,13.8443 18.444833,8.68095 z"
id="cl-9-8-9-6" /><path
style="fill:#000000"
inkscape:connector-curvature="0"
d="m 46.261118,149.87509 c 0,0 -5.967372,4.77391 -5.967372,11.39203 0,3.8744 3.43972,10.3066 11.392028,10.3066 7.952308,0 11.392028,-6.43471 11.392028,-10.3066 0,-6.61812 -5.967373,-11.39203 -5.967373,-11.39203 6.62818,5.16335 18.444833,1.6382 18.444833,-8.68095 0,-5.16586 -4.22113,-10.84931 -10.849311,-10.84931 -7.952308,0 -11.392027,8.68095 -11.392027,8.68095 0,0 -1.010056,-9.89453 4.881939,-15.19104 H 45.175685 c 5.891994,5.294 4.881938,15.19104 4.881938,15.19104 0,0 -3.439718,-8.68095 -11.392027,-8.68095 -6.630693,0 -10.849311,5.68596 -10.849311,10.84931 0,10.31915 11.816654,13.8443 18.444833,8.68095 z"
id="cl-9-8-0-4-9" /></svg>

After

Width:  |  Height:  |  Size: 14 KiB

Some files were not shown because too many files have changed in this diff Show More