Compare commits
16 Commits
1afecfd781
...
d8a27f8e41
Author | SHA1 | Date |
---|---|---|
Trevor Vallender | d8a27f8e41 | |
Trevor Vallender | 2aef1b532c | |
Trevor Vallender | b024ee829b | |
Trevor Vallender | 5f682dff6e | |
Trevor Vallender | 0658eca783 | |
Trevor Vallender | c2c733fcec | |
Trevor Vallender | 1b90ed389f | |
Trevor Vallender | 05ffcf0456 | |
Trevor Vallender | 1c2f8a5576 | |
Trevor Vallender | c01ab549fd | |
Trevor Vallender | bd21b53b06 | |
Trevor Vallender | adb472e543 | |
Trevor Vallender | 7ba4ae2b50 | |
Trevor Vallender | 781893f12f | |
Trevor Vallender | 7753e6d268 | |
Trevor Vallender | 8ac1a845f6 |
|
@ -49,3 +49,29 @@
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats, .counters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .2em;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat, .counter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
h6 {
|
||||||
|
font-size: .8em;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--header-text-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 3em;
|
||||||
|
width: 2.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
:root {
|
:root {
|
||||||
|
--text-color: #000;
|
||||||
--background-color: #333;
|
--background-color: #333;
|
||||||
--main-background-color:
|
--main-background-color:
|
||||||
--header-color: #15345b;
|
--header-color: #15345b;
|
||||||
|
|
|
@ -40,3 +40,17 @@ form, fieldset {
|
||||||
background-color: var(--input-background);
|
background-color: var(--input-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.stat-form, form.counter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--input-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number]:disabled {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,12 @@ class CharacterSheetSectionsController < ApplicationController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@sections = @character.character_sheet_sections.top_level
|
@sections = @character.character_sheet_sections.top_level
|
||||||
|
@editable = ActiveModel::Type::Boolean.new.cast(params[:editable])
|
||||||
|
if params[:table_id].present?
|
||||||
|
@table = Current.user.tables.find(params[:table_id])
|
||||||
|
@characters = Current.user.characters.where(table: @table)
|
||||||
|
render layout: "table"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
@ -17,6 +23,7 @@ class CharacterSheetSectionsController < ApplicationController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@section = @character.character_sheet_sections.new(character_sheet_section_params)
|
@section = @character.character_sheet_sections.new(character_sheet_section_params)
|
||||||
|
@editable = true
|
||||||
unless @section.save
|
unless @section.save
|
||||||
@parent_section = @section.parent_section
|
@parent_section = @section.parent_section
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CountersController < ApplicationController
|
||||||
|
before_action :set_section, only: [ :new, :create ]
|
||||||
|
before_action :set_character, only: [ :new, :create ]
|
||||||
|
before_action :set_counter, only: [ :update, :destroy ]
|
||||||
|
|
||||||
|
def new
|
||||||
|
@counter = @section.counters.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@counter = @section.counters.new(counter_params)
|
||||||
|
unless @counter.save
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@counter.update(counter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@id = helpers.dom_id(@counter)
|
||||||
|
@counter.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def set_character
|
||||||
|
@character = @section.character
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_section
|
||||||
|
@section = Current.user.character_sheet_sections.find(params[:character_sheet_section_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_counter
|
||||||
|
@counter = Current.user.counters.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def counter_params
|
||||||
|
params.require(:counter).permit(
|
||||||
|
:name,
|
||||||
|
:value,
|
||||||
|
:character_sheet_section_id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EventsController < ApplicationController
|
||||||
|
layout "table"
|
||||||
|
|
||||||
|
before_action :set_table
|
||||||
|
|
||||||
|
def index
|
||||||
|
@characters = Current.user.characters.where(table: @table)
|
||||||
|
@events = @table.dice_rolls.order(:created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_table
|
||||||
|
@table = Current.user.tables.find(params[:table_id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,49 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StatsController < ApplicationController
|
||||||
|
before_action :set_section, only: [ :new, :create ]
|
||||||
|
before_action :set_character, only: [ :new, :create ]
|
||||||
|
before_action :set_stat, only: [ :update, :destroy ]
|
||||||
|
|
||||||
|
def new
|
||||||
|
@stat = @section.stats.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@stat = @section.stats.new(stat_params)
|
||||||
|
unless @stat.save
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@stat.update(stat_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@id = helpers.dom_id(@stat)
|
||||||
|
@stat.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def set_character
|
||||||
|
@character = @section.character
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_section
|
||||||
|
@section = Current.user.character_sheet_sections.find(params[:character_sheet_section_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_stat
|
||||||
|
@stat = Current.user.stats.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def stat_params
|
||||||
|
params.require(:stat).permit(
|
||||||
|
:name,
|
||||||
|
:value,
|
||||||
|
:roll_command,
|
||||||
|
:character_sheet_section_id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class TablesController < ApplicationController
|
class TablesController < ApplicationController
|
||||||
before_action :set_table, only: [ :show, :edit, :update, :destroy ]
|
before_action :set_table, only: [ :show, :edit, :update, :destroy ]
|
||||||
|
before_action :set_characters, only: [ :show ]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@tables = Current.user.tables
|
@tables = Current.user.tables
|
||||||
|
@ -9,6 +10,7 @@ class TablesController < ApplicationController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@table_invites = @table.table_invites.not_responded
|
@table_invites = @table.table_invites.not_responded
|
||||||
|
render layout: "table"
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
@ -52,6 +54,10 @@ class TablesController < ApplicationController
|
||||||
@table = Current.user.tables.find(params[:id])
|
@table = Current.user.tables.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_characters
|
||||||
|
@characters = Current.user.characters.where(table: @table)
|
||||||
|
end
|
||||||
|
|
||||||
def table_params
|
def table_params
|
||||||
params.require(:table).permit(
|
params.require(:table).permit(
|
||||||
:name,
|
:name,
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {
|
||||||
|
updateUrl: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.element.addEventListener("change", this.#updateValue.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateValue() {
|
||||||
|
this.element.requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ class Character < ApplicationRecord
|
||||||
belongs_to :game_system
|
belongs_to :game_system
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :character_sheet_sections, dependent: :destroy
|
has_many :character_sheet_sections, dependent: :destroy
|
||||||
|
has_many :stats, through: :character_sheet_sections
|
||||||
|
|
||||||
validates :name, presence: true,
|
validates :name, presence: true,
|
||||||
length: { maximum: 200 }
|
length: { maximum: 200 }
|
||||||
|
|
|
@ -2,8 +2,13 @@
|
||||||
|
|
||||||
class CharacterSheetSection < ApplicationRecord
|
class CharacterSheetSection < ApplicationRecord
|
||||||
belongs_to :character
|
belongs_to :character
|
||||||
belongs_to :parent_section, optional: true, class_name: "CharacterSheetSection"
|
belongs_to :parent_section, optional: true,
|
||||||
has_many :character_sheet_subsections, class_name: "CharacterSheetSection", foreign_key: :parent_section_id
|
class_name: "CharacterSheetSection"
|
||||||
|
has_many :character_sheet_subsections, class_name: "CharacterSheetSection",
|
||||||
|
foreign_key: :parent_section_id,
|
||||||
|
dependent: :destroy
|
||||||
|
has_many :counters, dependent: :destroy
|
||||||
|
has_many :stats, dependent: :destroy
|
||||||
|
|
||||||
validates :name, presence: true,
|
validates :name, presence: true,
|
||||||
length: { maximum: 255 }
|
length: { maximum: 255 }
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Sluggable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
validates :slug, presence: true,
|
||||||
|
length: { maximum: 100 },
|
||||||
|
uniqueness: { scope: :character_sheet_section_id }
|
||||||
|
|
||||||
|
before_validation :set_slug
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_slug
|
||||||
|
return if slug.present? || name.blank?
|
||||||
|
|
||||||
|
slug = if character_sheet_section.parent_section.present?
|
||||||
|
[
|
||||||
|
character_sheet_section.parent_section.name.parameterize,
|
||||||
|
name.parameterize,
|
||||||
|
].join("-")
|
||||||
|
else
|
||||||
|
slug = name.parameterize
|
||||||
|
end
|
||||||
|
|
||||||
|
suffix = 2
|
||||||
|
while Stat.exists?(slug:)
|
||||||
|
slug = "#{slug}-#{suffix}"
|
||||||
|
suffix += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
self.slug = slug
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Counter < ApplicationRecord
|
||||||
|
include Sluggable
|
||||||
|
|
||||||
|
belongs_to :character_sheet_section
|
||||||
|
|
||||||
|
validates :name, presence: true,
|
||||||
|
length: { maximum: 100 },
|
||||||
|
uniqueness: { scope: :character_sheet_section_id }
|
||||||
|
validates :value, presence: true,
|
||||||
|
numericality: true
|
||||||
|
before_validation :set_slug
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DiceRoll < ApplicationRecord
|
||||||
|
belongs_to :rollable, polymorphic: true
|
||||||
|
belongs_to :table
|
||||||
|
|
||||||
|
validates :result, presence: true,
|
||||||
|
numericality: { only_integer: true }
|
||||||
|
|
||||||
|
def display_text
|
||||||
|
"#{rollable.character.name} rolled #{rollable.name}: <strong>#{result}</strong>".html_safe
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Stat < ApplicationRecord
|
||||||
|
include Sluggable
|
||||||
|
|
||||||
|
belongs_to :character_sheet_section
|
||||||
|
has_one :character, through: :character_sheet_section
|
||||||
|
has_many :dice_rolls, as: :rollable, dependent: :destroy
|
||||||
|
|
||||||
|
validates :name, presence: true,
|
||||||
|
length: { maximum: 100 },
|
||||||
|
uniqueness: { scope: :character_sheet_section_id }
|
||||||
|
validates :slug, presence: true,
|
||||||
|
length: { maximum: 100 },
|
||||||
|
uniqueness: true
|
||||||
|
validates :value, presence: true,
|
||||||
|
numericality: true
|
||||||
|
validate :validate_roll_command
|
||||||
|
|
||||||
|
def roll(table)
|
||||||
|
result = DiceRoller.new(roll_command, stat: self).roll
|
||||||
|
dice_rolls.create(result:, table:)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_roll_command
|
||||||
|
return if roll_command.blank?
|
||||||
|
|
||||||
|
DiceRoller.new(roll_command).valid?
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,6 +4,7 @@ class Table < ApplicationRecord
|
||||||
belongs_to :owner, class_name: "User"
|
belongs_to :owner, class_name: "User"
|
||||||
belongs_to :game_system
|
belongs_to :game_system
|
||||||
has_many :characters, dependent: :nullify
|
has_many :characters, dependent: :nullify
|
||||||
|
has_many :dice_rolls, dependent: :destroy
|
||||||
has_many :players, dependent: :destroy
|
has_many :players, dependent: :destroy
|
||||||
has_many :table_invites, dependent: :destroy
|
has_many :table_invites, dependent: :destroy
|
||||||
has_many :users, through: :players
|
has_many :users, through: :players
|
||||||
|
|
|
@ -9,6 +9,8 @@ class User < ApplicationRecord
|
||||||
has_many :character_sheet_sections, through: :characters
|
has_many :character_sheet_sections, through: :characters
|
||||||
has_many :owned_tables, foreign_key: :owner_id, class_name: "Table"
|
has_many :owned_tables, foreign_key: :owner_id, class_name: "Table"
|
||||||
has_many :players, dependent: :destroy
|
has_many :players, dependent: :destroy
|
||||||
|
has_many :counters, through: :character_sheet_sections
|
||||||
|
has_many :stats, through: :character_sheet_sections
|
||||||
has_many :tables, through: :players
|
has_many :tables, through: :players
|
||||||
has_rich_text :profile
|
has_rich_text :profile
|
||||||
has_one_attached :avatar do |attachable|
|
has_one_attached :avatar do |attachable|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DiceRoller
|
||||||
|
def initialize(roll_command, stat: nil)
|
||||||
|
@roll_command = roll_command
|
||||||
|
@stat = stat&.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def roll
|
||||||
|
result = 0
|
||||||
|
operator = nil
|
||||||
|
roll_command_parts.each do |part|
|
||||||
|
case part
|
||||||
|
when /\A\d*d\d+\z/
|
||||||
|
operator.nil? ? (result += roll_dice(part)) : (result = result.send(operator, roll_dice(part)))
|
||||||
|
when /\A\d+\z/
|
||||||
|
operator.nil? ? (result += part.to_i) : (result = result.send(operator, part.to_i))
|
||||||
|
when "self"
|
||||||
|
operator.nil? ? (result += @stat) : (result = result.send(operator, @stat))
|
||||||
|
when /\A[+\-*\/]\z/
|
||||||
|
operator = part
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
return if @roll_command.blank?
|
||||||
|
|
||||||
|
# No repeated math operators
|
||||||
|
return false if @roll_command.match?(/[+\-*\/]{2,}/)
|
||||||
|
|
||||||
|
# No leading or trailing math operators
|
||||||
|
return false if @roll_command.match?(/\A[+\-*\/]/) || @roll_command.match?(/[+\-*\/]\z/)
|
||||||
|
|
||||||
|
@roll_command.match?(
|
||||||
|
/
|
||||||
|
\A(
|
||||||
|
(\d*d\d*) |
|
||||||
|
([+\-*\/]) |
|
||||||
|
(\d+) |
|
||||||
|
(self)
|
||||||
|
)*\z/xi,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def roll_command_parts
|
||||||
|
@roll_command.scan(/([+\-*\/])|(\d*d\d+)|(\d+)|(self)/xi)
|
||||||
|
.flatten
|
||||||
|
.compact_blank
|
||||||
|
end
|
||||||
|
|
||||||
|
def roll_dice(command)
|
||||||
|
parts = command.downcase.split("d").compact_blank
|
||||||
|
die_type = parts.last
|
||||||
|
dice_number = parts.length > 1 ? parts.first.to_i : 1
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
dice_number.times do
|
||||||
|
result += rand(1..die_type.to_i)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,12 +4,20 @@
|
||||||
<h4><%= character_sheet_section.name %></h4>
|
<h4><%= character_sheet_section.name %></h4>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<div id="<%= dom_id(character_sheet_section) %>_stats" class="stats">
|
||||||
|
<%= render character_sheet_section.stats %>
|
||||||
|
</div>
|
||||||
|
<div id="<%= dom_id(character_sheet_section) %>_counters" class="counters">
|
||||||
|
<%= render character_sheet_section.counters %>
|
||||||
|
</div>
|
||||||
<div id="<%= dom_id(character_sheet_section) %>_sections">
|
<div id="<%= dom_id(character_sheet_section) %>_sections">
|
||||||
<%= render character_sheet_section.character_sheet_subsections %>
|
<%= render character_sheet_section.character_sheet_subsections %>
|
||||||
</div>
|
</div>
|
||||||
<div id="<%= dom_id(character_sheet_section) %>_add_section">
|
<% if @editable %>
|
||||||
<%= render partial: "edit_links",
|
<div id="<%= dom_id(character_sheet_section) %>_add_section">
|
||||||
locals: { section: character_sheet_section, parent: character_sheet_section, id: nil } %>
|
<%= render partial: "edit_links",
|
||||||
</div>
|
locals: { section: character_sheet_section, parent: character_sheet_section, id: nil } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
<%# locals: (section:, parent:, id:) -%>
|
<%# locals: (section:, parent:, id:) -%>
|
||||||
<%= link_to t(".add_subsection"),
|
<%= link_to t(".add_subsection"),
|
||||||
new_character_character_sheet_section_path(@character, parent_section_id: parent&.id),
|
new_character_character_sheet_section_path(@character, parent_section_id: parent&.id),
|
||||||
data: { turbo_stream: true } %>
|
data: { turbo_stream: true } %>
|
||||||
<% unless id == "character_sheet_add_section" %>
|
<% if section.present? %>
|
||||||
<%= link_to t(".delete_section", name: section.name), character_sheet_section_path(section),
|
<%= link_to t(".add_stat"),
|
||||||
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete", name: section.name) } %>
|
new_character_sheet_section_stat_path(section), data: { turbo_stream: true }, class: "add-stat" %>
|
||||||
|
<%= link_to t(".add_counter"),
|
||||||
|
new_character_sheet_section_counter_path(section), data: { turbo_stream: true }, class: "add-counter" %>
|
||||||
|
<% end %>
|
||||||
|
<% unless id == "character_sheet_add_section" %>
|
||||||
|
<%= link_to t(".delete_section", name: section.name), character_sheet_section_path(section),
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete", name: section.name) } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
<h2><%= @character.name %></h2>
|
<h2><%= @character.name %></h2>
|
||||||
|
|
||||||
|
<% if @editable %>
|
||||||
|
<%= link_to t(".stop_editing"), url_for(editable: false) %>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to t(".edit"), url_for(editable: true) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div id="character_sheet">
|
<div id="character_sheet">
|
||||||
<% if @sections.any? %>
|
<% if @sections.any? %>
|
||||||
<%= render @sections %>
|
<%= render @sections %>
|
||||||
|
@ -10,7 +16,9 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="character_sheet_add_section">
|
<% if @editable %>
|
||||||
<%= link_to t(".add_section"), new_character_character_sheet_section_path(@character),
|
<div id="character_sheet_add_section">
|
||||||
data: { turbo_stream: true } %>
|
<%= link_to t(".add_section"), new_character_character_sheet_section_path(@character),
|
||||||
</div>
|
data: { turbo_stream: true } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="counter" id="<%= dom_id(counter) %>">
|
||||||
|
<% if @editable %>
|
||||||
|
<%= link_to(t(".delete"), counter,
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete", name: counter.name) }) %>
|
||||||
|
<% end %>
|
||||||
|
<h6><%= counter.name %></h6>
|
||||||
|
<%= form_with model: counter, class: "counter-form",
|
||||||
|
data: { controller: "auto-update", auto_update_update_url_value: counter_path(counter) } do |f| %>
|
||||||
|
<%= f.number_field :value %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<section class="inset">
|
||||||
|
<%= form_with model: @counter, url: character_sheet_section_counters_path(@section) do |f| %>
|
||||||
|
<%= f.hidden_field :character_sheet_section_id, value: @section.id %>
|
||||||
|
|
||||||
|
<%= f.label :name %>
|
||||||
|
<%= f.text_field :name %>
|
||||||
|
<%= display_form_errors(@section, :name) %>
|
||||||
|
|
||||||
|
<%= f.submit button_text %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<%= content_target = "#{dom_id(@section)}_counters" %>
|
||||||
|
<%= turbo_stream.append(content_target) do %>
|
||||||
|
<%= render @counter %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_target = "#{dom_id(@section)}_add_section" %>
|
||||||
|
<%= turbo_stream.replace(form_target) do %>
|
||||||
|
<div id="#{dom_id(@section)}_add_section">
|
||||||
|
<%= render partial: "character_sheet_sections/edit_links", locals: { section: @section, parent: @section.parent_section, id: nil} %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -0,0 +1 @@
|
||||||
|
<%= turbo_stream.remove @id %>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<% target = "#{dom_id(@counter.character_sheet_section)}_add_section" %>
|
||||||
|
<%= turbo_stream.replace(target) do %>
|
||||||
|
<div id="<%= target %>">
|
||||||
|
<%= render partial: "form", locals: { button_text: t(".create_counter") } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<%= turbo_stream.replace dom_id(@counter) do %>
|
||||||
|
<%= render @counter %>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<li class="dice-roll">
|
||||||
|
<%= dice_roll.display_text %>
|
||||||
|
</li>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<% content_for :title, @table.name %>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<%= render @events %>
|
||||||
|
</ul>
|
|
@ -35,7 +35,8 @@
|
||||||
<main>
|
<main>
|
||||||
<%= render partial: "shared/flash_messages" %>
|
<%= render partial: "shared/flash_messages" %>
|
||||||
<%= yield(:submenu) if content_for?(:submenu) %>
|
<%= yield(:submenu) if content_for?(:submenu) %>
|
||||||
<%= yield %>
|
<%= yield(:main) if content_for?(:main) %>
|
||||||
|
<%= yield unless content_for?(:main) %>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<% content_for :submenu do %>
|
||||||
|
<h2><%= @table.name %></h2>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><%= link_to t(".overview"), @table %></li>
|
||||||
|
<% @characters.each do |character| %>
|
||||||
|
<li><%= link_to character.name, table_character_character_sheet_sections_path(@table, character) %></li>
|
||||||
|
<% end %>
|
||||||
|
<li><%= link_to t(".events"), table_events_path(@table) %></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% content_for :main do %>
|
||||||
|
<%= turbo_frame_tag("table-content") do %>
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= render template: "layouts/application" %>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<section class="inset">
|
||||||
|
<%= form_with model: @stat, url: character_sheet_section_stats_path(@section) do |f| %>
|
||||||
|
<%= f.hidden_field :character_sheet_section_id, value: @section.id %>
|
||||||
|
|
||||||
|
<%= f.label :name %>
|
||||||
|
<%= f.text_field :name %>
|
||||||
|
<%= display_form_errors(@section, :name) %>
|
||||||
|
|
||||||
|
<%= f.label :roll_command %>
|
||||||
|
<%= f.text_field :roll_command %>
|
||||||
|
<%= display_form_errors(@section, :roll_command) %>
|
||||||
|
|
||||||
|
<%= f.submit button_text %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="stat" id="<%= dom_id(stat) %>">
|
||||||
|
<% if @editable %>
|
||||||
|
<%= link_to(t(".delete"), stat,
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete", name: stat.name) }) %>
|
||||||
|
<% end %>
|
||||||
|
<h6><%= stat.name %></h6>
|
||||||
|
<%= form_with model: stat, class: "stat-form",
|
||||||
|
data: { controller: "auto-update", auto_update_update_url_value: stat_path(stat) } do |f| %>
|
||||||
|
<%= f.number_field :value, disabled: !@editable %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<%= content_target = "#{dom_id(@section)}_stats" %>
|
||||||
|
<%= turbo_stream.append(content_target) do %>
|
||||||
|
<%= render @stat %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_target = "#{dom_id(@section)}_add_section" %>
|
||||||
|
<%= turbo_stream.replace(form_target) do %>
|
||||||
|
<div id="#{dom_id(@section)}_add_section">
|
||||||
|
<%= render partial: "character_sheet_sections/edit_links", locals: { section: @section, parent: @section.parent_section, id: nil} %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -0,0 +1 @@
|
||||||
|
<%= turbo_stream.remove @id %>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<% target = "#{dom_id(@stat.character_sheet_section)}_add_section" %>
|
||||||
|
<%= turbo_stream.replace(target) do %>
|
||||||
|
<div id="<%= target %>">
|
||||||
|
<%= render partial: "form", locals: { button_text: t(".create_stat") } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<%= turbo_stream.replace dom_id(@stat) do %>
|
||||||
|
<%= render @stat %>
|
||||||
|
<% end %>
|
|
@ -1,7 +1,5 @@
|
||||||
<% content_for :title, @table.name %>
|
<% content_for :title, @table.name %>
|
||||||
|
|
||||||
<h2><%= @table.name %></h2>
|
|
||||||
|
|
||||||
<%= link_to t(".invite_user"), new_table_table_invite_path(@table) %>
|
<%= link_to t(".invite_user"), new_table_table_invite_path(@table) %>
|
||||||
<%= link_to t(".edit_table"), edit_table_path(@table) %>
|
<%= link_to t(".edit_table"), edit_table_path(@table) %>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"check_name": "Render",
|
"check_name": "Render",
|
||||||
"message": "Render path contains parameter value",
|
"message": "Render path contains parameter value",
|
||||||
"file": "app/views/tables/show.html.erb",
|
"file": "app/views/tables/show.html.erb",
|
||||||
"line": 17,
|
"line": 15,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
"code": "render(action => Current.user.tables.find(params[:id]).characters, {})",
|
"code": "render(action => Current.user.tables.find(params[:id]).characters, {})",
|
||||||
"render_path": [
|
"render_path": [
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
"type": "controller",
|
"type": "controller",
|
||||||
"class": "TablesController",
|
"class": "TablesController",
|
||||||
"method": "show",
|
"method": "show",
|
||||||
"line": 12,
|
"line": 13,
|
||||||
"file": "app/controllers/tables_controller.rb",
|
"file": "app/controllers/tables_controller.rb",
|
||||||
"rendered": {
|
"rendered": {
|
||||||
"name": "tables/show",
|
"name": "tables/show",
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
"check_name": "Render",
|
"check_name": "Render",
|
||||||
"message": "Render path contains parameter value",
|
"message": "Render path contains parameter value",
|
||||||
"file": "app/views/character_sheet_sections/index.html.erb",
|
"file": "app/views/character_sheet_sections/index.html.erb",
|
||||||
"line": 7,
|
"line": 13,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
"code": "render(action => Current.user.characters.find(params[:character_id]).character_sheet_sections.top_level, {})",
|
"code": "render(action => Current.user.characters.find(params[:character_id]).character_sheet_sections.top_level, {})",
|
||||||
"render_path": [
|
"render_path": [
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
"type": "controller",
|
"type": "controller",
|
||||||
"class": "CharacterSheetSectionsController",
|
"class": "CharacterSheetSectionsController",
|
||||||
"method": "index",
|
"method": "index",
|
||||||
"line": 9,
|
"line": 10,
|
||||||
"file": "app/controllers/character_sheet_sections_controller.rb",
|
"file": "app/controllers/character_sheet_sections_controller.rb",
|
||||||
"rendered": {
|
"rendered": {
|
||||||
"name": "character_sheet_sections/index",
|
"name": "character_sheet_sections/index",
|
||||||
|
@ -67,8 +67,42 @@
|
||||||
22
|
22
|
||||||
],
|
],
|
||||||
"note": ""
|
"note": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "Dynamic Render Path",
|
||||||
|
"warning_code": 15,
|
||||||
|
"fingerprint": "8cc5f8414893bc6d142fda5a2a91346bfe171640647dc017315ab21724170912",
|
||||||
|
"check_name": "Render",
|
||||||
|
"message": "Render path contains parameter value",
|
||||||
|
"file": "app/views/events/index.html.erb",
|
||||||
|
"line": 4,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
|
"code": "render(action => Current.user.tables.find(params[:table_id]).dice_rolls.order(:created_at), {})",
|
||||||
|
"render_path": [
|
||||||
|
{
|
||||||
|
"type": "controller",
|
||||||
|
"class": "EventsController",
|
||||||
|
"method": "index",
|
||||||
|
"line": 11,
|
||||||
|
"file": "app/controllers/events_controller.rb",
|
||||||
|
"rendered": {
|
||||||
|
"name": "events/index",
|
||||||
|
"file": "app/views/events/index.html.erb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"location": {
|
||||||
|
"type": "template",
|
||||||
|
"template": "events/index"
|
||||||
|
},
|
||||||
|
"user_input": "params[:table_id]",
|
||||||
|
"confidence": "Weak",
|
||||||
|
"cwe_id": [
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"note": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2024-06-06 14:18:01 +0100",
|
"updated": "2024-06-13 13:24:46 +0100",
|
||||||
"brakeman_version": "6.1.2"
|
"brakeman_version": "6.1.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,9 @@ en:
|
||||||
See you soon,
|
See you soon,
|
||||||
The Tabletop Companion team
|
The Tabletop Companion team
|
||||||
sign_off_html: "<p>See you soon,<br>The Tabletop Companion team</p>"
|
sign_off_html: "<p>See you soon,<br>The Tabletop Companion team</p>"
|
||||||
|
table:
|
||||||
|
overview: Overview
|
||||||
|
events: Events
|
||||||
account_verifications:
|
account_verifications:
|
||||||
show:
|
show:
|
||||||
success: "Thanks for verifying your email address! You can now log in."
|
success: "Thanks for verifying your email address! You can now log in."
|
||||||
|
@ -60,7 +63,11 @@ en:
|
||||||
add_subsection: Add subsection
|
add_subsection: Add subsection
|
||||||
delete_section: Delete %{name}
|
delete_section: Delete %{name}
|
||||||
confirm_delete: Are you sure you want to delete this section?
|
confirm_delete: Are you sure you want to delete this section?
|
||||||
|
add_stat: Add stat
|
||||||
|
add_counter: Add counter
|
||||||
index:
|
index:
|
||||||
|
edit: Edit character sheet
|
||||||
|
stop_editing: Stop editing
|
||||||
character_sheet: "%{name}’s character sheet"
|
character_sheet: "%{name}’s character sheet"
|
||||||
no_sections: This character sheet has no content
|
no_sections: This character sheet has no content
|
||||||
add_section: Add section
|
add_section: Add section
|
||||||
|
@ -94,6 +101,12 @@ en:
|
||||||
destroy:
|
destroy:
|
||||||
success: Deleted “%{name}”
|
success: Deleted “%{name}”
|
||||||
error: Could not delete your character
|
error: Could not delete your character
|
||||||
|
counters:
|
||||||
|
counter:
|
||||||
|
delete: Delete counter
|
||||||
|
confirm_delete: Are you sure you want to delete %{name}?
|
||||||
|
new:
|
||||||
|
create_counter: Create counter
|
||||||
password_resets:
|
password_resets:
|
||||||
new:
|
new:
|
||||||
reset_password: Reset your password
|
reset_password: Reset your password
|
||||||
|
@ -129,6 +142,12 @@ en:
|
||||||
destroy:
|
destroy:
|
||||||
log_out: Log out
|
log_out: Log out
|
||||||
success: "You have signed out."
|
success: "You have signed out."
|
||||||
|
stats:
|
||||||
|
stat:
|
||||||
|
delete: Delete
|
||||||
|
confirm_delete: Are you sure you want to delete %{name}?
|
||||||
|
new:
|
||||||
|
create_stat: Create stat
|
||||||
table_invite_mailer:
|
table_invite_mailer:
|
||||||
invite_new_user:
|
invite_new_user:
|
||||||
subject: You’ve been invited to join a game on Tabletop Companion!
|
subject: You’ve been invited to join a game on Tabletop Companion!
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
default_url_options host: "summonplayer.com"
|
default_url_options host: "ttcompanion.com"
|
||||||
|
|
||||||
root "tables#index"
|
root "tables#index"
|
||||||
|
|
||||||
|
@ -15,12 +15,21 @@ Rails.application.routes.draw do
|
||||||
resources :password_resets, only: [ :new, :create, :edit, :update ]
|
resources :password_resets, only: [ :new, :create, :edit, :update ]
|
||||||
resources :sessions, only: [ :new, :create, :destroy ]
|
resources :sessions, only: [ :new, :create, :destroy ]
|
||||||
|
|
||||||
resources :character_sheet_sections, only: [ :destroy ]
|
resources :character_sheet_sections, only: [ :destroy ] do
|
||||||
|
resources :counters, only: [ :new, :create ]
|
||||||
|
resources :stats, only: [ :new, :create ]
|
||||||
|
end
|
||||||
resources :characters do
|
resources :characters do
|
||||||
resources :character_sheet_sections, only: [ :index, :new, :create ]
|
resources :character_sheet_sections, only: [ :index, :new, :create ]
|
||||||
end
|
end
|
||||||
|
resources :counters, only: [ :update, :destroy ]
|
||||||
|
resources :stats, only: [ :update, :destroy ]
|
||||||
resources :table_invites, only: [ :index, :edit, :update ]
|
resources :table_invites, only: [ :index, :edit, :update ]
|
||||||
resources :tables do
|
resources :tables do
|
||||||
|
resources :characters, only: [] do
|
||||||
|
resources :character_sheet_sections, only: [ :index ]
|
||||||
|
end
|
||||||
|
resources :events, only: [ :index ]
|
||||||
resources :table_invites, only: [ :new, :create ]
|
resources :table_invites, only: [ :new, :create ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateStats < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :stats do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :slug, null: false
|
||||||
|
t.belongs_to :character_sheet_section, null: false, foreign_key: true
|
||||||
|
t.integer :value, null: false, default: 0
|
||||||
|
t.string :roll_command
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_check_constraint :stats, "length(name) <= 100", name: "chk_stat_name_max_length"
|
||||||
|
add_check_constraint :stats, "length(slug) <= 100", name: "chk_stat_slug_max_length"
|
||||||
|
|
||||||
|
add_index :stats, :slug, unique: true
|
||||||
|
add_index :stats, [ :name, :character_sheet_section_id ], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateCounters < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :counters do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :slug, null: false
|
||||||
|
t.integer :value, null: false, default: 0
|
||||||
|
t.belongs_to :character_sheet_section, null: false, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_check_constraint :counters, "length(name) <= 100", name: "chk_counter_name_max_length"
|
||||||
|
add_check_constraint :counters, "length(slug) <= 100", name: "chk_counter_slug_max_length"
|
||||||
|
|
||||||
|
add_index :counters, :slug, unique: true
|
||||||
|
add_index :counters, [ :name, :character_sheet_section_id ], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateDiceRolls < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :dice_rolls do |t|
|
||||||
|
t.references :rollable, polymorphic: true
|
||||||
|
t.belongs_to :table, null: true, foreign_key: true
|
||||||
|
t.integer :result, null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_06_05_175553) do
|
ActiveRecord::Schema[7.1].define(version: 2024_06_13_072942) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -76,6 +76,31 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_05_175553) do
|
||||||
t.check_constraint "length(name::text) <= 200", name: "chk_character_name_max_length"
|
t.check_constraint "length(name::text) <= 200", name: "chk_character_name_max_length"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "counters", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.string "slug", null: false
|
||||||
|
t.integer "value", default: 0, null: false
|
||||||
|
t.bigint "character_sheet_section_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["character_sheet_section_id"], name: "index_counters_on_character_sheet_section_id"
|
||||||
|
t.index ["name", "character_sheet_section_id"], name: "index_counters_on_name_and_character_sheet_section_id", unique: true
|
||||||
|
t.index ["slug"], name: "index_counters_on_slug", unique: true
|
||||||
|
t.check_constraint "length(name::text) <= 100", name: "chk_counter_name_max_length"
|
||||||
|
t.check_constraint "length(slug::text) <= 100", name: "chk_counter_slug_max_length"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "dice_rolls", force: :cascade do |t|
|
||||||
|
t.string "rollable_type"
|
||||||
|
t.bigint "rollable_id"
|
||||||
|
t.bigint "table_id"
|
||||||
|
t.integer "result", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["rollable_type", "rollable_id"], name: "index_dice_rolls_on_rollable"
|
||||||
|
t.index ["table_id"], name: "index_dice_rolls_on_table_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "game_systems", force: :cascade do |t|
|
create_table "game_systems", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
@ -209,6 +234,21 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_05_175553) do
|
||||||
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
|
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "stats", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.string "slug", null: false
|
||||||
|
t.bigint "character_sheet_section_id", null: false
|
||||||
|
t.integer "value", default: 0, null: false
|
||||||
|
t.string "roll_command"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["character_sheet_section_id"], name: "index_stats_on_character_sheet_section_id"
|
||||||
|
t.index ["name", "character_sheet_section_id"], name: "index_stats_on_name_and_character_sheet_section_id", unique: true
|
||||||
|
t.index ["slug"], name: "index_stats_on_slug", unique: true
|
||||||
|
t.check_constraint "length(name::text) <= 100", name: "chk_stat_name_max_length"
|
||||||
|
t.check_constraint "length(slug::text) <= 100", name: "chk_stat_slug_max_length"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "table_invites", force: :cascade do |t|
|
create_table "table_invites", force: :cascade do |t|
|
||||||
t.bigint "table_id", null: false
|
t.bigint "table_id", null: false
|
||||||
t.string "email", null: false
|
t.string "email", null: false
|
||||||
|
@ -261,6 +301,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_05_175553) do
|
||||||
add_foreign_key "characters", "game_systems"
|
add_foreign_key "characters", "game_systems"
|
||||||
add_foreign_key "characters", "tables"
|
add_foreign_key "characters", "tables"
|
||||||
add_foreign_key "characters", "users"
|
add_foreign_key "characters", "users"
|
||||||
|
add_foreign_key "counters", "character_sheet_sections"
|
||||||
|
add_foreign_key "dice_rolls", "tables"
|
||||||
add_foreign_key "players", "tables"
|
add_foreign_key "players", "tables"
|
||||||
add_foreign_key "players", "users"
|
add_foreign_key "players", "users"
|
||||||
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
||||||
|
@ -269,6 +311,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_05_175553) do
|
||||||
add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
||||||
add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
||||||
add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
||||||
|
add_foreign_key "stats", "character_sheet_sections"
|
||||||
add_foreign_key "table_invites", "tables"
|
add_foreign_key "table_invites", "tables"
|
||||||
add_foreign_key "tables", "game_systems"
|
add_foreign_key "tables", "game_systems"
|
||||||
add_foreign_key "tables", "users", column: "owner_id"
|
add_foreign_key "tables", "users", column: "owner_id"
|
||||||
|
|
|
@ -29,7 +29,7 @@ class CharacterSheetSectionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
user = users(:trevor)
|
user = users(:trevor)
|
||||||
sign_in user
|
sign_in user
|
||||||
assert_difference "CharacterSheetSection.count", -1 do
|
assert_difference "CharacterSheetSection.count", -1 do
|
||||||
delete character_sheet_section_url(user.character_sheet_sections.first),
|
delete character_sheet_section_url(character_sheet_sections(:subsection)),
|
||||||
as: :turbo_stream
|
as: :turbo_stream
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CountersControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
test "should render new turbo stream" do
|
||||||
|
sign_in users(:trevor)
|
||||||
|
get new_character_sheet_section_counter_url(character_sheet_sections(:counters)), as: :turbo_stream
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should create counter" do
|
||||||
|
sign_in users(:trevor)
|
||||||
|
assert_difference "Counter.count", 1 do
|
||||||
|
post character_sheet_section_counters_url(character_sheet_sections(:counters)),
|
||||||
|
params: { counter: { name: "Ammo", character_sheet_section_id: character_sheet_sections(:counters).id } },
|
||||||
|
as: :turbo_stream
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should delete counter" do
|
||||||
|
sign_in users(:trevor)
|
||||||
|
assert_difference "Counter.count", -1 do
|
||||||
|
delete counter_url(Counter.first), as: :turbo_stream
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class StatsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
test "should render new turbo stream" do
|
||||||
|
sign_in users(:trevor)
|
||||||
|
get new_character_sheet_section_stat_url(character_sheet_sections(:stats)), as: :turbo_stream
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should create stat" do
|
||||||
|
sign_in users(:trevor)
|
||||||
|
assert_difference "Stat.count", 1 do
|
||||||
|
post character_sheet_section_stats_url(character_sheet_sections(:stats)),
|
||||||
|
params: { stat: { name: "Wisdom", character_sheet_section_id: character_sheet_sections(:stats).id } },
|
||||||
|
as: :turbo_stream
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should delete stat" do
|
||||||
|
sign_in users(:trevor)
|
||||||
|
assert_difference "Stat.count", -1 do
|
||||||
|
delete stat_url(Stat.first), as: :turbo_stream
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,3 +1,12 @@
|
||||||
stats:
|
stats:
|
||||||
name: Stats
|
name: Stats
|
||||||
character: nardren
|
character: nardren
|
||||||
|
|
||||||
|
counters:
|
||||||
|
name: Status
|
||||||
|
character: nardren
|
||||||
|
|
||||||
|
subsection:
|
||||||
|
name: Subsection
|
||||||
|
character: nardren
|
||||||
|
parent_section: stats
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
hp:
|
||||||
|
name: HP
|
||||||
|
value: 10
|
||||||
|
slug: hp
|
||||||
|
character_sheet_section: counters
|
|
@ -0,0 +1,4 @@
|
||||||
|
one:
|
||||||
|
rollable: strength (Stat)
|
||||||
|
table: dnd_table
|
||||||
|
result: 14
|
|
@ -0,0 +1,6 @@
|
||||||
|
strength:
|
||||||
|
name: Strength
|
||||||
|
slug: strength
|
||||||
|
character_sheet_section: stats
|
||||||
|
value: 10
|
||||||
|
roll_command: d20+self
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DiceRollingTest < ActionDispatch::IntegrationTest
|
||||||
|
test "rolling a stat should create a dice roll" do
|
||||||
|
assert_changes("DiceRoll.count", +1) do
|
||||||
|
stats(:strength).roll(tables(:dnd_table))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class EditCharacterSheetTest < ActionDispatch::IntegrationTest
|
||||||
|
test "Sheet is only editable when parameter is set" do
|
||||||
|
user = users(:trevor)
|
||||||
|
sign_in user
|
||||||
|
|
||||||
|
get character_character_sheet_sections_path(characters(:nardren))
|
||||||
|
assert_response :success
|
||||||
|
assert_no_match I18n.t("character_sheet_sections.index.add_section"), response.body
|
||||||
|
|
||||||
|
get character_character_sheet_sections_path(characters(:nardren), editable: true)
|
||||||
|
assert_response :success
|
||||||
|
assert_match I18n.t("character_sheet_sections.index.add_section"), response.body
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CounterTest < ActiveSupport::TestCase
|
||||||
|
test "name must exist" do
|
||||||
|
assert_must_exist(counters(:hp), :name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "value must exist" do
|
||||||
|
assert_must_exist(counters(:hp), :value)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DiceRollTest < ActiveSupport::TestCase
|
||||||
|
test "result must exist" do
|
||||||
|
assert_must_exist dice_rolls(:one), :result
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class StatTest < ActiveSupport::TestCase
|
||||||
|
test "name must exist" do
|
||||||
|
assert_must_exist(stats(:strength), :name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "value must exist" do
|
||||||
|
assert_must_exist(stats(:strength), :value)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saving generates appropriate slug" do
|
||||||
|
stat = Stat.create(name: "Foo", character_sheet_section: character_sheet_sections(:subsection))
|
||||||
|
assert_equal "stats-foo", stat.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates unique slug always" do
|
||||||
|
existing_stat = Stat.first
|
||||||
|
stat = Stat.create(name: existing_stat.name, character_sheet_section: existing_stat.character_sheet_section)
|
||||||
|
assert_not_equal existing_stat.slug, stat.slug
|
||||||
|
assert_equal "#{existing_stat.slug}-2", stat.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rolls with roll_command" do
|
||||||
|
stat = stats(:strength)
|
||||||
|
stat.roll_command = "1d6"
|
||||||
|
100.times { assert (1..6).cover?(stat.roll(tables(:dnd_table))) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DiceRollerTest < ActiveSupport::TestCase
|
||||||
|
test "correctly validates strings" do
|
||||||
|
valid_strings = [
|
||||||
|
"2d6",
|
||||||
|
"12",
|
||||||
|
"self",
|
||||||
|
"d12",
|
||||||
|
"d20+self",
|
||||||
|
"8+D8-self",
|
||||||
|
]
|
||||||
|
|
||||||
|
invalid_strings = [
|
||||||
|
"+",
|
||||||
|
"sel+10",
|
||||||
|
"d8++13",
|
||||||
|
]
|
||||||
|
|
||||||
|
valid_strings.each do |roll_command|
|
||||||
|
assert DiceRoller.new(roll_command).valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
invalid_strings.each do |roll_command|
|
||||||
|
assert_not DiceRoller.new(roll_command).valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rolls appropriate results" do
|
||||||
|
100.times do
|
||||||
|
assert (1..6).include? DiceRoller.new("d6").roll
|
||||||
|
assert (2..12).include? DiceRoller.new("2d6").roll
|
||||||
|
assert (11..30).include? DiceRoller.new("d20+self", stat: stats(:strength)).roll
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,8 @@ class CharacterSheetTest < ApplicationSystemTestCase
|
||||||
click_on I18n.t("characters.show.sheet")
|
click_on I18n.t("characters.show.sheet")
|
||||||
assert_text character.name
|
assert_text character.name
|
||||||
|
|
||||||
|
click_on I18n.t("character_sheet_sections.index.edit")
|
||||||
|
|
||||||
click_link(I18n.t("character_sheet_sections.index.add_section"))
|
click_link(I18n.t("character_sheet_sections.index.add_section"))
|
||||||
fill_in attr_name(CharacterSheetSection, :name), with: "Test Section"
|
fill_in attr_name(CharacterSheetSection, :name), with: "Test Section"
|
||||||
click_button(I18n.t("character_sheet_sections.new.create_section"))
|
click_button(I18n.t("character_sheet_sections.new.create_section"))
|
||||||
|
@ -19,5 +21,10 @@ class CharacterSheetTest < ApplicationSystemTestCase
|
||||||
click_link(I18n.t("character_sheet_sections.edit_links.delete_section", name: "Test Section"))
|
click_link(I18n.t("character_sheet_sections.edit_links.delete_section", name: "Test Section"))
|
||||||
accept_confirm
|
accept_confirm
|
||||||
assert_no_text "Test Section"
|
assert_no_text "Test Section"
|
||||||
|
|
||||||
|
first(".add-stat").click
|
||||||
|
fill_in attr_name(Stat, :name), with: "Test Stat"
|
||||||
|
click_button I18n.t("stats.new.create_stat")
|
||||||
|
assert_text "Test Stat"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
13
todo.md
13
todo.md
|
@ -1,8 +1,17 @@
|
||||||
|
- character sheet on table
|
||||||
|
- table tabs
|
||||||
|
- roll on click
|
||||||
|
- Sheets
|
||||||
|
- Lists
|
||||||
|
- Text fields
|
||||||
|
- Weapons/spells
|
||||||
|
- Character avatars
|
||||||
- default avatars
|
- default avatars
|
||||||
- add uuid/slug to characters and any other url-visible ids
|
- add uuid/slug to characters and any other url-visible ids
|
||||||
- shared/private notes
|
- shared/private notes
|
||||||
- Add characters to users/tables
|
- NPCs
|
||||||
- Character sheets/prototypes
|
- Character sheets prototypes
|
||||||
- notifications
|
- notifications
|
||||||
- chat
|
- chat
|
||||||
- maps
|
- maps
|
||||||
|
- show errors from invalid sheet bits
|
||||||
|
|
Loading…
Reference in New Issue