3 커밋 50c7f2df9c ... 1256c50f91

작성자 SHA1 메시지 날짜
  Kylie Jo Swistak 1256c50f91 correct inconsistent title 4 년 전
  Kylie Jo Swistak aed8ca6a2f add aliases and alternate form sections to characters 4 년 전
  Kylie Jo Swistak 455ab27852 add journal entries and navigation options for them 4 년 전

+ 4 - 7
app/carousels/image.rb

@@ -24,7 +24,7 @@ class ImageCarousel < Carousel
       image = CharImage.find(carousel.image_id)
       character = Character.find(image.char_id)
 
-      # Transition into an ImageCarousel
+      # Transition into a MemberCarousel
       event.message.delete_all_reactions
       CharacterCarousel.transition(event, carousel, character)
     when 'left', 'right'
@@ -68,17 +68,14 @@ class ImageCarousel < Carousel
       char_id: nil,
       image_id: image&.id,
       landmark_id: nil,
-      options: nil
+      options: nil,
+      journal_page: nil
     )
 
-    # Array of section reactions and an X
-    img_reactions = sections.map{ |k,v| k }
-    img_reactions.push(Emoji::CROSS)
-
     # Update reply
     BotResponse.new(
       carousel: carousel,
-      reactions: img_reactions,
+      reactions: sections.map{ |k,v| k }.push(Emoji::CROSS),
       embed: character_embed(
         character: character,
         event: event,

+ 84 - 0
app/carousels/journal.rb

@@ -0,0 +1,84 @@
+require './app/models/carousels.rb'
+require './lib/emoji.rb'
+
+class JournalCarousel < Carousel
+  def self.sections
+    {
+      Emoji::UNDO => 'back',
+      Emoji::LEFT => 'left',
+      Emoji::RIGHT => 'right',
+    }
+  end
+
+  def self.update_embed(event, carousel)
+    # Save reactions and determine section
+    reactions = event.message.reactions
+    direction = sections.filter{ |k,v| reactions[k]&.count.to_i > 1 }.values.first
+
+    # Close if X is chosen
+    return carousel.close(event) if reactions[Emoji::CROSS]&.count.to_i > 1
+
+    case direction
+    when 'back'
+      # Fetch character
+      character = Character.find(carousel.char_id)
+
+      # Transition into a MemberCarousel
+      event.message.delete_all_reactions
+      CharacterCarousel.transition(event, carousel, character)
+    when 'left', 'right'
+      # Fetch the correspoding emoji, and remove non-bot reactions
+      emoji = sections.key(direction)
+      event.message.reacted_with(emoji).each do |r|
+        event.message.delete_reaction(r.id, emoji) unless r.current_bot?
+      end
+
+      # Next Journal Page
+      page = JournalController.journal_scroll(
+        char_id: carousel.char_id,
+        page: carousel.journal_page,
+        dir: direction
+      )
+
+      # Update Carousel
+      carousel.update(char_id: carousel.char_id, journal_page: page)
+
+      # Update embed with new page
+      BotResponse.new(
+        carousel: carousel,
+        embed: character_embed(
+          character: Character.find(carousel.char_id),
+          event: event,
+          section: 'journal',
+          journal: JournalController.fetch_page(carousel.char_id, page)
+        )
+      )
+    end
+  end
+
+  def self.transition(event, carousel, character)
+    # Fetch inital page of journals
+    journals = JournalController.fetch_page(character.id, 1)
+
+    # Update carousel to reflect new information
+    carousel.update(
+      char_id: character.id,
+      image_id: nil,
+      landmark_id: nil,
+      options: nil,
+      journal_page: 1
+    )
+
+    # Update Reply
+    BotResponse.new(
+      carousel: carousel,
+      reactions: sections.map{ |k,v| k }.push(Emoji::CROSS),
+      embed: character_embed(
+        character: character,
+        event: event,
+        section: 'journal',
+        journal: journals
+      )
+    )
+  end
+end

+ 17 - 2
app/carousels/member.rb

@@ -3,8 +3,10 @@ class CharacterCarousel < Carousel
     {
       Emoji::EYES => 'all',
       Emoji::PICTURE => 'image',
+      Emoji::NOTEBOOK => 'journal',
       Emoji::BAGS => 'bags',
       Emoji::FAMILY => 'family',
+      Emoji::SPY => 'forms',
       Emoji::BUST => 'user'
     }
   end
@@ -22,6 +24,10 @@ class CharacterCarousel < Carousel
       # Transition into an ImageCarousel
       event.message.delete_all_reactions
       ImageCarousel.transition(event, carousel, Character.find(carousel.char_id))
+    when 'journal'
+      # Transition into an JournalCarousel
+      event.message.delete_all_reactions
+      JournalCarousel.transition(event, carousel, Character.find(carousel.char_id))
     when 'user'
       # Find User
       character = Character.find(carousel.char_id)
@@ -29,7 +35,15 @@ class CharacterCarousel < Carousel
 
       # Transition into a UserCarousel
       event.message.delete_all_reactions
-      UserCarousel.transition(event, carousel, user)
+      UserCarousel.transition_user(event, carousel, user)
+    when 'forms'
+      # Find User
+      character = Character.find(carousel.char_id)
+      base_char = character.alt_form ? Character.find(character.alt_form) : character
+
+      # Transition into a UserCarousel
+      event.message.delete_all_reactions
+      UserCarousel.transition_character(event, carousel, base_char)
     when 'all', 'bags', 'family'
       # Fetch the corresponding emoji, and remove non-bot reactions
       emoji = sections.key(section)
@@ -55,7 +69,8 @@ class CharacterCarousel < Carousel
       char_id: character.id,
       image_id: nil,
       landmark_id: nil,
-      options: nil
+      options: nil,
+      journal_page: nil
     )
 
     # Update reply

+ 29 - 2
app/carousels/user.rb

@@ -16,7 +16,7 @@ class UserCarousel < Carousel
     CharacterCarousel.transition(event, carousel, character)
   end
 
-  def self.transition(event, carousel, user)
+  def self.transition_user(event, carousel, user)
     # Character array
     all_chars = Character.where(user_id: user.id).order(:rating)
 
@@ -32,7 +32,8 @@ class UserCarousel < Carousel
       char_id: nil,
       image_id: nil,
       landmark_id: nil,
-      options: chars.map{ |c| c.id }
+      options: chars.map{ |c| c.id },
+      journal_page: nil
     )
 
     # Array of section reactions and an X
@@ -47,4 +48,30 @@ class UserCarousel < Carousel
       embed: user_char_embed(all_chars, member, sfw)
     )
   end
+
+  def self.transition_character(event, carousel, character)
+    # Character array
+    chars = [ character ]
+    chars.concat( Character.where(alt_form: character.id) )
+
+    # Update carousel to reflect new information
+    carousel.update(
+      char_id: nil,
+      image_id: nil,
+      landmark_id: nil,
+      options: chars.map{ |c| c.id },
+      journal_page: nil
+    )
+
+    # Array of section reactions and an X
+    user_reactions = Emoji::NUMBERS.take(chars.length)
+    user_reactions.push(Emoji::CROSS)
+
+    # Update reply
+    BotResponse.new(
+      carousel: carousel,
+      reactions: user_reactions,
+      embed: character_embed(character: character, event: event, section: 'forms')
+    )
+  end
 end

+ 63 - 0
app/commands/journal.rb

@@ -0,0 +1,63 @@
+require './app/commands/base_command.rb'
+
+class JournalCommand < BaseCommand
+  def self.opts
+    {
+      usage: {
+        character: "Searches for the character by name, can only add entries for your own characters",
+        title: "A title for the journal, may be blank (defaults to date)",
+        entry: "The journal entry, should be a paragraph as the character might enter into a diary"
+      }
+    }
+  end
+
+  def self.cmd
+    desc = "Create a short journal entry for a character"
+
+    @cmd ||= Command.new(:journal, desc, opts) do |event, name, title, note|
+      # Find the character
+      character = Character.restricted_find(name, event.author, ['Archived'])
+
+      # Format and create Journal
+      date = Time.now.strftime("%a, %b %d, %Y")
+      if !note
+        note = title
+        title = date
+      elsif title == ''
+        title = date
+      end
+
+      # Create a new Journal Entry with formatted date
+      journal = JournalEntry.create(
+        char_id: character.id,
+        title: title,
+        date: date,
+        entry: note
+      )
+
+      # Create response embed and reply
+      BotResponse.new(
+        embed: character_embed(
+          character: character,
+          event: event,
+          section: :journal,
+          journal: journal
+        )
+      )
+
+    rescue ActiveRecord::RecordNotFound => e
+      error_embed("Record not Found!", e.message)
+    end
+  end
+
+  def self.example_command(event)
+    journal_entry_examples = [
+      "Today I did a thing, and it was fun. Yay!",
+      "As I walk through the valley where I harvest my grain, I take a look at my wife and realize she's very plain",
+      "I want to kill a mother fucker just to see how it feels"
+    ]
+
+    [Character.where(active: 'Active').order('RANDOM()').first.name,
+     journal_entry_examples.sample]
+  end
+end

+ 55 - 9
app/commands/member.rb

@@ -7,6 +7,7 @@ class MemberCommand < BaseCommand
       nav: {
         all: [ Emoji::EYES, "View all info about the character" ],
         image: [ Emoji::PICTURE, "Scroll though the character's images" ],
+        journal: [ Emoji::NOTEBOOK, "Scroll though pages of journal entries" ],
         bags: [ Emoji::BAGS, "View the character's inventory" ],
         family: [ Emoji::FAMILY, "View related characters" ],
         user: [ Emoji::BUST, "View the writer's other characters in a list" ]
@@ -20,8 +21,8 @@ class MemberCommand < BaseCommand
         "Skips to the specified section, some options include: bio, type, status, " +
         "rumors, image, bags. If no section is given, R0ry will default to history",
         keyword:
-        "Displays a specific image, searched by its title, or keyword. " +
-        "Can only be used if the section option is `image`",
+        "Displays a specific image or journal, searched by its title, or keyword. " +
+        "Can only be used if the section option is `image` or `journal`",
       }
     }
   end
@@ -78,6 +79,8 @@ class MemberCommand < BaseCommand
         else
           character = Character.where.not(active: 'Deleted')
             .where('name ilike ?', name)
+            .or(Character.where.not(active: 'Deleted')
+            .where('? = any(aliases)', name))
           raise 'Character not found!' if character.empty?
         end
 
@@ -114,11 +117,25 @@ class MemberCommand < BaseCommand
      end
     end
 
-    # Find image if specified
-    image = CharImage.where(char_id: character.id).
-      find_by('keyword ilike ?', keyword || 'Default')
+    case section
+    when /image/i
+      # Find image if specified
+      image = CharImage.where(char_id: character.id).
+        find_by('keyword ilike ?', keyword || 'Default')
 
-    raise 'Image not found!' if keyword && !image
+      raise 'Image not found!' if keyword && !image
+    when /journal/i
+      # Find journal if specified
+      if keyword
+        journal = JournalEntry.where(char_id: character.id).
+          find_by('title ilike ?', keyword)
+
+        raise 'Journal not found!' if !journal
+      else
+        # Fetch Journal list if no keyword
+        journal = JournalEntry.where(char_id: character.id).take(10)
+      end
+    end
 
     # Ensure the content is appropriate for the current channel
     if sfw && ( image&.category == 'NSFW' || character.rating == 'NSFW' )
@@ -130,7 +147,8 @@ class MemberCommand < BaseCommand
       character: character,
       event: event,
       section: section,
-      image: image
+      image: image,
+      journal: journal
     )
 
     # Determine Carousel Type and create reply
@@ -140,6 +158,31 @@ class MemberCommand < BaseCommand
         carousel: image,
         reactions: ImageCarousel.sections.map{ |k,v| k }.push(Emoji::CROSS)
       )
+
+    elsif section&.match(/journal/i)
+      journal = journal.first unless journal.is_a? JournalEntry
+
+      BotResponse.new(
+        embed: embed,
+        carousel: journal,
+        reactions: JournalCarousel.sections.map{ |k,v| k }.push(Emoji::CROSS)
+      )
+    elsif section&.match(/(alt(ernate)?)?\s?forms?/i)
+      chars = []
+      if character.alt_form
+        chars.push( Character.find(character.alt_form) )
+      else
+        chars.push(character)
+      end
+
+      # Add forms
+      chars.concat( Character.where(alt_form: chars.first.id) )
+
+      BotResponse.new(
+        embed: embed,
+        carousel: chars.map{ |c| c.id },
+        reactions: Emoji::NUMBERS.take(chars.length).push(Emoji::CROSS)
+      )
     else
       BotResponse.new(
         embed: embed,
@@ -150,7 +193,7 @@ class MemberCommand < BaseCommand
   end
 
   def self.example_command(event=nil)
-    sections = ['all', 'bio', 'type', 'status', 'rumors', 'image', 'bags']
+    sections = ['all', 'bio', 'type', 'status', 'rumors', 'image', 'bags', 'journal']
 
     case ['', 'user', 'name', 'section', 'keyword'].sample
     when ''
@@ -166,7 +209,10 @@ class MemberCommand < BaseCommand
        sections.sample]
     when 'keyword'
       i = CharImage.where.not(keyword: 'Default').order('RANDOM()').first
-      [Character.find(i.char_id).name, 'image', i.keyword]
+      j = JournalEntry.order('RANDOM()').first
+
+      [[Character.find(i.char_id).name, 'image', i.keyword],
+       [Character.find(j.char_id).name, 'journal', j.title || j.date]].sample
     end
   end
 end

+ 21 - 0
app/controllers/journal_controller.rb

@@ -0,0 +1,21 @@
+class JournalController
+  def self.journal_scroll(char_id:, page:, dir:)
+    total_journals = JournalEntry.where(char_id: char_id).length
+
+    new_page = case dir
+               when :left
+                 page <= 1 ? total_journals / 10 + 1 : page - 1
+               when :right
+                 page >= total_journals / 10 + 1 ? 1 : page + 1
+               else
+                 1
+               end
+
+    new_page
+  end
+
+  def self.fetch_page(char_id, page)
+    JournalEntry.where(char_id: char_id).
+      slice(page*10 - 10 .. page*10-1)
+  end
+end

+ 55 - 2
app/embeds/character.rb

@@ -1,4 +1,4 @@
-def character_embed(character:, event:, section: nil, image: nil)
+def character_embed(character:, event:, section: nil, image: nil, journal: nil)
   # Find the author, if they're a member, or in DMs use the event's author
   if event.server
     member = character.user_id.match(/public/i) ? 'Public' :
@@ -23,6 +23,7 @@ def character_embed(character:, event:, section: nil, image: nil)
   case section
   when /all/i, /default/i, nil
     embed.description = character.personality if character.personality
+    fields = char_alias(character, fields)
     fields = char_type(character, fields)
     fields = char_status(character, fields)
     fields = char_bio(character, fields)
@@ -30,6 +31,7 @@ def character_embed(character:, event:, section: nil, image: nil)
     fields = char_dm_notes(character, fields, event)
   when /bio/i
     embed.description = character.personality if character.personality
+    fields = char_alias(character, fields)
     fields = char_bio(character, fields)
     fields = char_dm_notes(character, fields, event)
   when /types?/i
@@ -56,6 +58,10 @@ def character_embed(character:, event:, section: nil, image: nil)
     embed.thumbnail = nil
   when /bags?/i, /inventory/i
     fields = char_inv(character, fields)
+  when /journal/i
+    fields = char_journal(character, fields, journal)
+  when /forms?/i, /alt(ernate)?\sforms?/i
+    fields = alt_form_embed(character, fields)
   end
 
   # Add fields to embed
@@ -66,6 +72,14 @@ def character_embed(character:, event:, section: nil, image: nil)
   author_footer(embed, member, footer_info)
 end
 
+def char_alias(char, fields)
+  fields.push(
+    { name: 'Known Aliases', value: char.aliases.join(', ') }
+  )if char.aliases
+
+  fields
+end
+
 def char_bio(char, fields)
   # Find the appropriate teams
   char_teams = CharTeam.where(char_id: char.id, active: true).map(&:team_id)
@@ -187,6 +201,21 @@ def char_inv(char, fields)
   fields
 end
 
+def char_journal(char, fields, journal)
+  if journal.is_a? JournalEntry
+    fields.push({ name: journal.title, value: journal.entry })
+  elsif journal.empty?
+    fields.push({ name: 'Error', value: 'No journal entries found' })
+  else
+    # Display each journal entry
+    journal.each do |j|
+      fields.push({ name: j&.title || j.date, value: j.entry })
+    end
+  end
+
+  fields
+end
+
 def char_dm_notes(char, fields, event)
   return fields unless ENV['DM_CH'].include?(event.channel.id.to_s)
 
@@ -318,7 +347,7 @@ def user_char_embed(chars, member, nsfw=nil)
 
   active.each.with_index do |char, i|
     name = nsfw && char.rating == 'NSFW' ?
-      "#{i+1} || #{char.name} ||" : "#{i+1} #{char.name}"
+      "#{Emoji::NUMBERS[i]} || #{char.name} ||" : "#{Emoji::NUMBERS[i]} #{char.name}"
     fields.push({
       name: name,
       value: "#{char.species} -- #{char.types}"
@@ -366,6 +395,30 @@ def dup_char_embed(chars, name)
   )
 end
 
+def alt_form_embed(char, fields)
+  # Find Base Character Form
+  chars = []
+  if char.alt_form
+    chars.push( Character.find(char.alt_form) )
+  else
+    chars.push(char)
+  end
+
+  # Add forms
+  chars.concat( Character.where(alt_form: chars.first.id) )
+
+  # Display forms
+  chars.each.with_index do |char, i|
+    fields.push({
+      name: "#{Emoji::NUMBERS[i]} #{char.name}",
+      value: "#{char.species} -- #{char.types}"
+    })
+  end
+
+  # return fields
+  fields
+end
+
 def image_list_embed(character, event)
   # Find the author, if they're a member, or in DMs use the event's author
   if event.server

+ 2 - 0
app/models/bot_response.rb

@@ -43,6 +43,8 @@ class BotResponse
       Carousel.create(message_id: message.id, landmark_id: @carousel.id)
     when Fable
       Carousel.create(message_id: message.id, fable_id: @carousel.id)
+    when JournalEntry
+      Carousel.create(message_id: message.id, char_id: @carousel.char_id, journal_page: 1)
     when CharImage
       Carousel.create(
         message_id: message.id,

+ 3 - 0
app/models/carousels.rb

@@ -6,6 +6,9 @@ class Carousel < ActiveRecord::Base
     if options
       # User List
       UserCarousel.update_embed(event, self)
+    elsif journal_page
+      # Journal
+      JournalCarousel.update_embed(event, self)
     elsif char_id
       # Character
       CharacterCarousel.update_embed(event, self)

+ 9 - 1
app/models/characters.rb

@@ -33,6 +33,7 @@ class Character < ActiveRecord::Base
   def self.from_form(app)
     key_mapping = {
       "Characters Name" => "name",
+      "Aliases" => "aliases",
       "Species" => "species",
       "Shiny" => "shiny",
       "Type" => "types",
@@ -55,12 +56,14 @@ class Character < ActiveRecord::Base
       "Rating" => "rating",
       "Current Location" => "location",
       "DM Notes" => "dm_notes",
+      "Base Character ID" => "alt_form",
       "Edit Key (ignore)" => "edit_url",
     }
 
     hash = {
       "user_id" => nil,
       "name" => nil,
+      "aliases" => nil,
       "species" => nil,
       "shiny" => nil,
       "types" => nil,
@@ -84,13 +87,16 @@ class Character < ActiveRecord::Base
       "rumors" => nil,
       "hometown" => nil,
       "warnings" => nil,
-      "rating" => nil
+      "rating" => nil,
+      "alt_form" => nil,
     }
 
     user_id = UID.match(app.description)
     active = case app.title
              when /Personal Character/
                'Active'
+             when /Alternative Form/
+               'Alt Form'
              when /NPC/
                'NPC'
              when /Archived Character/
@@ -116,6 +122,8 @@ class Character < ActiveRecord::Base
 
       if db_column == 'shiny'
         hash[db_column] = field.value.match(/yes/i) ? true : false
+      elsif db_column == 'aliases'
+        hash[db_column] = field.value.split(/\s?\|\s?/)
       else
         hash[db_column] = field.value
       end

+ 5 - 0
app/models/journal_entry.rb

@@ -0,0 +1,5 @@
+class JournalEntry < ActiveRecord::Base
+  validates :char_id, presence: true
+  validates :date, presence: true
+  validates :entry, presence: true
+end

+ 2 - 1
lib/emoji.rb

@@ -81,6 +81,7 @@ module Emoji
   BUST = "👤"
   WIZARD = "🧙"
   TALK = "🗣️"
+  SPY = "🕵️"
 
   GHOST = "👻"
   FISH = "🐟"
@@ -94,7 +95,7 @@ module Emoji
 
   CHAR_APP = [SPEECH_BUBBLE, PICTURE, BOOKS, NOTE, QUESTION, PEOPLE, WIZARD]
   IMG_APP = [DOG, KEY, FLAG, PAGE, BOOKS, VULGAR]
-  MEMBER = [EYES, PICTURE, BAGS, FAMILY, BUST, CROSS]
+  MEMBER = [EYES, PICTURE, NOTEBOOK, BAGS, FAMILY, SPY, BUST, CROSS]
   LANDMARK = [BOOKS, SKULL, MAP, HOUSES, PEOPLE, CROSS]
 
   APPLICATION = [Y, N, CRAYON, CROSS]