feat: implement basic fairy tracker

This commit is contained in:
insects 2025-03-11 17:47:32 +01:00
parent 1e46ad269e
commit 8109bacb0d
27 changed files with 290 additions and 7 deletions

View file

@ -1,6 +1,6 @@
body { body {
font-family: sans-serif; font-family: sans-serif;
margin: 10px 40px; margin: 10px 25px;
} }
header { header {
@ -31,6 +31,7 @@ header .muted {
main { main {
display: grid; display: grid;
gap: 5px;
grid-template-columns: 5fr 1fr; grid-template-columns: 5fr 1fr;
} }
@ -214,3 +215,49 @@ span#password {
color: #555; color: #555;
} }
.fairies .map {
position: relative;
}
.fairies .fairy-dot {
position: absolute;
vertical-align: top;
margin-top: -9px;
margin-left: -1px;
}
.fairy-marker {
font-weight: bold;
font-size: 12px;
}
.fairy-a {
color: #cf0e43;
}
.fairy-b {
color: gold;
}
.fairy-c {
color: aqua;
}
.fairy-list {
margin-top: 5px;
font-weight: bold;
display: flex;
flex-direction: column;
padding: 5px 10px;
}
.fairy-list > div {
display: flex;
justify-content: space-between;
}
.fairy-list .note {
font-weight: normal;
color: #555;
font-size: 14px;
}

View file

@ -0,0 +1,34 @@
class FairyController < ApplicationController
def mark
instance_id, location = mark_params
instance = Instance.find_by(public_id: instance_id)
fairy = Fairy.new(location: location, instance_id: instance.id)
unless params[:pwd]
fairy.is_suggested = true
end
if fairy.save
new_instance = Instance.includes(:fairies).find(instance.id)
render partial: "instance/fairies", locals: { instance: new_instance }
end
end
def unmark
instance_id, location = mark_params
if params[:pwd]
instance = Instance.find_by(public_id: instance_id)
Fairy.delete_by(instance_id: instance.id, location: location)
new_instance = Instance.includes(:fairies).find(instance.id)
render partial: "instance/fairies", locals: { instance: new_instance }
end
end
def despawn
end
private
def mark_params
params.expect(:instance, :location)
end
end

View file

@ -13,7 +13,7 @@ class InstanceController < ApplicationController
end end
def show def show
@instance = Instance.includes(:pops).find_by(public_id: show_instance_params) @instance = Instance.includes(:pops, :fairies).find_by(public_id: show_instance_params)
@forecast = Weather.forecast(@instance.zone.to_sym) @forecast = Weather.forecast(@instance.zone.to_sym)
end end
@ -23,7 +23,7 @@ class InstanceController < ApplicationController
if parent_instance.password == pwd if parent_instance.password == pwd
pop = Pop.new(instance_id: parent_instance.id, name: nm) pop = Pop.new(instance_id: parent_instance.id, name: nm)
if pop.save if pop.save
@instance = Instance.includes(:pops).find_by(public_id: instance_id) @instance = Instance.includes(:pops, :fairies).find_by(public_id: instance_id)
@forecast = Weather.forecast(@instance.zone.to_sym) @forecast = Weather.forecast(@instance.zone.to_sym)
render partial: "list", locals: { instance: @instance, forecast: @forecast } render partial: "list", locals: { instance: @instance, forecast: @forecast }
end end
@ -35,7 +35,7 @@ class InstanceController < ApplicationController
parent_instance = Instance.find_by(public_id: instance_id) parent_instance = Instance.find_by(public_id: instance_id)
if parent_instance.password == pwd if parent_instance.password == pwd
Pop.delete_by(instance_id: parent_instance.id, name: nm) Pop.delete_by(instance_id: parent_instance.id, name: nm)
@instance = Instance.includes(:pops).find_by(public_id: instance_id) @instance = Instance.includes(:pops, :fairies).find_by(public_id: instance_id)
@forecast = Weather.forecast(@instance.zone.to_sym) @forecast = Weather.forecast(@instance.zone.to_sym)
render partial: "list", locals: { instance: @instance, forecast: @forecast } render partial: "list", locals: { instance: @instance, forecast: @forecast }
end end

View file

@ -0,0 +1,2 @@
module FairyHelper
end

View file

@ -19,4 +19,20 @@ module InstanceHelper
end end
false false
end end
def get_map_x(zone)
case zone
when "anemos" then 1584
end
end
def get_map_y(zone)
case zone
when "anemos" then 1249
end
end
def has_fairy?(instance, fairy)
instance.fairies.any? { |f| "#{fairy[:x]},#{fairy[:y]}" == f.location }
end
end end

View file

@ -13,7 +13,7 @@ function checkPwd() {
el.classList.add("hidden"); el.classList.add("hidden");
}); });
const buttons = document.querySelectorAll(".action button"); const buttons = document.querySelectorAll(".action");
buttons.forEach(btn => { buttons.forEach(btn => {
const oldUrl= btn.getAttribute("hx-post"); const oldUrl= btn.getAttribute("hx-post");
btn.setAttribute("hx-post", `${oldUrl}&pwd=${pwd}`); btn.setAttribute("hx-post", `${oldUrl}&pwd=${pwd}`);

3
app/models/fairy.rb Normal file
View file

@ -0,0 +1,3 @@
class Fairy < ApplicationRecord
belongs_to :instance
end

View file

@ -1,5 +1,6 @@
class Instance < ApplicationRecord class Instance < ApplicationRecord
has_many :pops has_many :pops
has_many :fairies
validates :zone, inclusion: { in: %w[anemos pagos pyros hydatos] } validates :zone, inclusion: { in: %w[anemos pagos pyros hydatos] }
end end

View file

@ -0,0 +1,2 @@
<h1>Fairy#despawn</h1>
<p>Find me in app/views/fairy/despawn.html.erb</p>

View file

@ -0,0 +1,2 @@
<h1>Fairy#mark</h1>
<p>Find me in app/views/fairy/mark.html.erb</p>

View file

@ -0,0 +1,2 @@
<h1>Fairy#suggest</h1>
<p>Find me in app/views/fairy/suggest.html.erb</p>

View file

@ -0,0 +1,2 @@
<h1>Fairy#unmark</h1>
<p>Find me in app/views/fairy/unmark.html.erb</p>

View file

@ -0,0 +1,83 @@
<div class="fairies" id="fairies">
<div class="map">
<img src="/maps/<%= instance.zone %>.jpg" style="width: 100%;" />
<% APP_DATA[instance.zone.to_sym][:fairies].each do |fairy| %>
<div
class="<%= class_names(unclaimed: !has_fairy?(instance, fairy)) %> fairy-dot"
style="left: <%= fairy[:x].to_f / get_map_x(instance.zone) * 100 %>%; top: <%= fairy[:y].to_f / get_map_y(instance.zone) * 100 %>%; ">
<% if has_fairy?(instance, fairy) %>
<% idx = instance.fairies.index { |f| f.location == "#{fairy[:x]},#{fairy[:y]}" } %>
<% if idx == 0 %>
<span class="fairy-a fairy-marker">A</span>
<% elsif idx == 1 %>
<span class="fairy-b fairy-marker">B</span>
<% elsif idx == 2 %>
<span class="fairy-c fairy-marker">C</span>
<% else %>
<span class="other-fairy fairy-marker"><%= idx + 1 %></span>
<% end %>
<% else %>
<div
class="action button"
hx-post="/fairy/mark?instance=<%= instance.public_id %>&location=<%= fairy[:x] %>,<%= fairy[:y] %>"
hx-swap="outerHTML"
hx-target="#fairies"
hx-select="#fairies"
>
</div>
<% end %>
</div>
<% end %>
</div>
<% if instance.fairies.empty? %>
<div class="fairy-list">
<div class="note">No elementals found yet! Click on any dot to mark that position.</div>
</div>
<% else %>
<div class="fairy-list">
<% instance.fairies.to_a.each_index do |idx| %>
<div>
<% if idx == 0 %>
<div>
Elemental <span class="fairy-a">A</span>
</div>
<% elsif idx == 1 %>
<div>
Elemental <span class="fairy-b">B</span>
</div>
<% elsif idx == 2 %>
<div>
Elemental <span class="fairy-c">C</span>
</div>
<% elsif idx > 2 %>
<div>
Elemental <span><%= idx + 1 %></span>
</div>
<% end %>
<div>
<div
class="copyable action needs_pwd"
hx-post="/fairy/unmark?instance=<%= instance.public_id %>&location=<%= instance.fairies[idx].location %>"
hx-swap="outerHTML"
hx-target="#fairies"
hx-select="#fairies"
>
remove</div>
</div>
</div>
<% end %>
<div class="note">Click on a black dot to mark another Elemental.</div>
<div class="note">
<div
class="copyable"
data-copy-content="Elementals: <%= instance.fairies[0..-1].map { |f| f.location }.join(", ") %>"
>
copy elemental positions for chat
</div>
</div>
<% end %>
</div>
</div>

View file

@ -56,12 +56,16 @@
class="action reset" class="action reset"
hx-post="/reset?instance=<%= @instance.public_id %>&nm=<%= nm[:name].parameterize %>" hx-post="/reset?instance=<%= @instance.public_id %>&nm=<%= nm[:name].parameterize %>"
hx-target="#nm-list" hx-target="#nm-list"
hx-select="#nm-list"
hx-swap="outerHTML"
>Reset</button> >Reset</button>
<% else %> <% else %>
<button <button
class="action" class="action"
hx-post="/pop?instance=<%= @instance.public_id %>&nm=<%= nm[:name].parameterize %>" hx-post="/pop?instance=<%= @instance.public_id %>&nm=<%= nm[:name].parameterize %>"
hx-target="#nm-list" hx-target="#nm-list"
hx-select="#nm-list"
hx-swap="outerHTML"
>Pop</button> >Pop</button>
<% end %> <% end %>
</div> </div>

View file

@ -40,6 +40,10 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<div>
<%= render partial: "fairies", locals: { instance: @instance } %>
</div>
</div> </div>
</main> </main>

View file

@ -5,6 +5,8 @@ Rails.application.routes.draw do
post "/pop", to: "instance#pop", as: :pop_in_instance post "/pop", to: "instance#pop", as: :pop_in_instance
post "/reset", to: "instance#reset", as: :reset_in_instance post "/reset", to: "instance#reset", as: :reset_in_instance
post "/auth", to: "instance#authenticate", as: :authenticate_to_instance post "/auth", to: "instance#authenticate", as: :authenticate_to_instance
post "/fairy/mark", to: "fairy#mark", as: :mark_fairy
post "/fairy/unmark", to: "fairy#unmark", as: :unmark_fairy
get "/:public_id", to: "instance#show", as: :show_instance get "/:public_id", to: "instance#show", as: :show_instance
get "up" => "rails/health#show", as: :rails_health_check get "up" => "rails/health#show", as: :rails_health_check

View file

@ -5,6 +5,41 @@ weather = [
["snow", 10] ["snow", 10]
] ]
fairies = [
{ x = 322, y = 125 },
{ x = 444, y = 169 },
{ x = 453, y = 309 },
{ x = 134, y = 455 },
{ x = 204, y = 566 },
{ x = 180, y = 815 },
{ x = 373, y = 1001 },
{ x = 384, y = 731 },
{ x = 349, y = 678 },
{ x = 424, y = 634 },
{ x = 567, y = 467 },
{ x = 656, y = 513 },
{ x = 675, y = 600 },
{ x = 577, y = 699 },
{ x = 808, y = 1107 },
{ x = 914, y = 1047 },
{ x = 829, y = 873 },
{ x = 905, y = 738 },
{ x = 834, y = 540 },
{ x = 798, y = 304 },
{ x = 698, y = 150 },
{ x = 921, y = 401 },
{ x = 1056, y = 466 },
{ x = 1200, y = 543 },
{ x = 1034, y = 616 },
{ x = 1140, y = 756 },
{ x = 1216, y = 862 },
{ x = 1481, y = 591 },
{ x = 1438, y = 242 },
{ x = 1170, y = 161 },
{ x = 1058, y = 240 },
{ x = 1204, y = 308 }
]
[[nms]] [[nms]]
name = "Sabotender Corrido" name = "Sabotender Corrido"
level = 1 level = 1

View file

@ -8,5 +8,7 @@ class CreateInstances < ActiveRecord::Migration[8.0]
t.timestamps t.timestamps
end end
add_index :instances, :public_id
end end
end end

View file

@ -1,7 +1,7 @@
class CreateFairies < ActiveRecord::Migration[8.0] class CreateFairies < ActiveRecord::Migration[8.0]
def change def change
create_table :fairies do |t| create_table :fairies do |t|
t.integer :location t.string :location
t.boolean :is_despawned t.boolean :is_despawned
t.boolean :is_suggested t.boolean :is_suggested
t.references :instance, null: false, foreign_key: true t.references :instance, null: false, foreign_key: true

3
db/schema.rb generated
View file

@ -15,7 +15,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_10_183912) do
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
create_table "fairies", force: :cascade do |t| create_table "fairies", force: :cascade do |t|
t.integer "location" t.string "location"
t.boolean "is_despawned" t.boolean "is_despawned"
t.boolean "is_suggested" t.boolean "is_suggested"
t.bigint "instance_id", null: false t.bigint "instance_id", null: false
@ -31,6 +31,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_10_183912) do
t.string "zone" t.string "zone"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["public_id"], name: "index_instances_on_public_id"
end end
create_table "pops", force: :cascade do |t| create_table "pops", force: :cascade do |t|

BIN
public/maps/anemos.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

BIN
public/maps/hydatos.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

BIN
public/maps/pagos.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

BIN
public/maps/pyros.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

View file

@ -0,0 +1,23 @@
require "test_helper"
class FairyControllerTest < ActionDispatch::IntegrationTest
test "should get mark" do
get fairy_mark_url
assert_response :success
end
test "should get suggest" do
get fairy_suggest_url
assert_response :success
end
test "should get unmark" do
get fairy_unmark_url
assert_response :success
end
test "should get despawn" do
get fairy_despawn_url
assert_response :success
end
end

11
test/fixtures/fairies.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# This model initially had no columns defined. If you add columns to the
# model remove the "{}" from the fixture names and add the columns immediately
# below each fixture, per the syntax in the comments below
#
one: {}
# column: value
#
two: {}
# column: value

View file

@ -0,0 +1,7 @@
require "test_helper"
class FairyTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end