feat: add weather forecasting

This commit is contained in:
insects 2025-03-11 14:57:28 +01:00
parent 6eea0dc29f
commit 705e7b1dc0
20 changed files with 181 additions and 19 deletions

View file

@ -8,7 +8,7 @@ gem "importmap-rails"
gem "turbo-rails"
gem "stimulus-rails"
gem "jbuilder"
gem "toml-rb"
gem "tomlrb"
gem "nanoid"
gem "spicy-proton"

View file

@ -94,7 +94,6 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
citrus (3.0.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
crass (1.0.6)
@ -299,9 +298,7 @@ GEM
thruster (0.1.12-x86_64-darwin)
thruster (0.1.12-x86_64-linux)
timeout (0.4.3)
toml-rb (3.0.1)
citrus (~> 3.0, > 3.0)
racc (~> 1.7)
tomlrb (2.0.3)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
@ -358,7 +355,7 @@ DEPENDENCIES
spicy-proton
stimulus-rails
thruster
toml-rb
tomlrb
turbo-rails
tzinfo-data
web-console

View file

@ -29,6 +29,11 @@ header .muted {
font-weight: normal;
}
main {
display: grid;
grid-template-columns: 5fr 1fr;
}
.new-buttons {
display: flex;
gap: 5px;
@ -169,3 +174,26 @@ span#password {
.copyable:hover {
cursor: pointer;
}
.sidebar {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px 0;
}
.weather-list {
display: flex;
align-items: center;
gap: 3px;
}
.weather {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 12px;
color: #555;
}

View file

@ -1,8 +1,2 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "htmx.org"
import htmx from "htmx.org"
htmx.logger = function(elt, event, data) {
if(console) {
console.log(event, elt, data);
}
}

View file

@ -1,4 +1,4 @@
<main id="nm-list">
<div id="nm-list">
<% APP_DATA[instance.zone.to_sym][:nms].each do |nm| %>
<% is_popped = instance.pops.filter { |pop| (Time.current - 120.minutes) <= pop.created_at }.any? { |pop| pop.name == nm[:name].parameterize } %>
<section class="<%= class_names(popped: is_popped) %>">
@ -10,7 +10,7 @@
<span class="badge">LV<%= nm[:level].to_s.rjust(2, "0") %></span>
<%= nm[:name] %>
<% if nm[:weather] %>
<img src="/<%= nm[:weather] %>.png" title="during <%= nm[:weather] %> only" width="15" />
<img src="/weather/<%= nm[:weather] %>.png" title="during <%= nm[:weather] %> only" width="15" />
<% end %>
</h3>
<div class="spawn-info">
@ -20,7 +20,7 @@
<span title="only at night">🌙</span>
<% end %>
<% if nm[:spawned_by][:weather] %>
<img src="/<%= nm[:spawned_by][:weather] %>.png" title="during <%= nm[:spawned_by][:weather] %> only" width="15" />
<img src="/weather/<%= nm[:spawned_by][:weather] %>.png" title="during <%= nm[:spawned_by][:weather] %> only" width="15" />
<% end %>
<small class="badge">LV<%= nm[:spawned_by][:level].to_s.rjust(2, "0") %></small>
</div>
@ -52,4 +52,4 @@
</div>
</section>
<% end %>
</main>
</div>

View file

@ -22,7 +22,26 @@
</div>
</header>
<%= render partial: "list", locals: { instance: @instance } %>
<main>
<%= render partial: "list", locals: { instance: @instance } %>
<div class="sidebar">
<div class="weather-list">
<% forecast = Weather.forecast(@instance.zone.to_sym) %>
<div class="weather">
<img src="/weather/<%= forecast[0][:curr_weather] %>.png" width="25" title="<%= forecast[0][:weather_name] %>" />
<div>now</div>
</div>
<% 4.times do |i| %>
»
<div class="weather">
<img src="/weather/<%= forecast[i + 1][:curr_weather] %>.png" width="25" title="<%= forecast[i + 1][:weather_name] %>" />
<div><%= ((forecast[i + 1][:time] - Time.now.utc) / 1.minutes).floor %>m</div>
</div>
<% end %>
</div>
</div>
</main>
<%= javascript_include_tag "list" %>
</div>

View file

@ -1,5 +1,11 @@
anemos_data = TomlRB.load_file("./data/anemos.toml", symbolize_keys: true)
anemos_data = Tomlrb.load_file("./data/anemos.toml", symbolize_keys: true)
pagos_data = Tomlrb.load_file("./data/pagos.toml", symbolize_keys: true)
pyros_data = Tomlrb.load_file("./data/pyros.toml", symbolize_keys: true)
hydatos_data = Tomlrb.load_file("./data/hydatos.toml", symbolize_keys: true)
APP_DATA = {
anemos: anemos_data
anemos: anemos_data,
pagos: pagos_data,
pyros: pyros_data,
hydatos: hydatos_data
}

View file

@ -1,3 +1,10 @@
weather = [
["fair", 30],
["gales", 30],
["showers", 30],
["snow", 10]
]
[[nms]]
name = "Sabotender Corrido"
level = 1

7
data/hydatos.toml Normal file
View file

@ -0,0 +1,7 @@
weather = [
["fair", 12],
["showers", 22],
["gloom", 22],
["thunder", 22],
["snow", 22]
]

8
data/pagos.toml Normal file
View file

@ -0,0 +1,8 @@
weather = [
["fair", 10],
["fog", 18],
["heat", 18],
["snow", 18],
["thunder", 18],
["blizzards", 18]
]

8
data/pyros.toml Normal file
View file

@ -0,0 +1,8 @@
weather = [
["fair", 10],
["heat", 18],
["thunder", 18],
["blizzards", 18],
["umbral_wind", 18],
["snow", 18]
]

24
lib/clock.rb Normal file
View file

@ -0,0 +1,24 @@
class Clock
EARTH_TO_EORZEA = 3600.0 / 175.0
EORZEA_TO_EARTH = 1.0 / EARTH_TO_EORZEA
def self.get_current_eorzea_time
to_eorzea_time(Time.now.utc)
end
def self.to_eorzea_time(earth_time)
et_ts = earth_time.to_i
new_ts = et_ts.abs * EARTH_TO_EORZEA
Time.at(new_ts)
end
def self.to_earth_time(ez_time)
ez_ts = ez_time.to_i
new_ts = (ez_ts * EORZEA_TO_EARTH).ceil
Time.at(new_ts)
end
def self.display_ez_time(ez_time)
ez_time.strftime("%H:%M")
end
end

64
lib/weather.rb Normal file
View file

@ -0,0 +1,64 @@
class Weather
def self.get_weather(zone, hash = hash(get_seed))
total = 0
APP_DATA[zone][:weather].each do |arr|
name, rate = arr
if (total += rate) > hash
return name
end
end
end
def self.forecast(zone, seed = get_seed, count = 10)
res = []
prev_hash = hash(seed - 1)
prev_weather = get_weather(zone, prev_hash)
count.times do |_|
curr_hash = hash(seed)
curr_weather = get_weather(zone, curr_hash)
res.push({ zone: zone, prev_weather: prev_weather, curr_weather: curr_weather, weather_name: get_weather_name(curr_weather), seed: seed, time: Time.at(seed * 1400000.0 / 1000) })
prev_hash = curr_hash
prev_weather = curr_weather
seed += 1
end
res
end
def self.round_to_last_weather_time(time)
last_hour = time.hour / 8 * 8
time.change(hour: last_hour)
end
def self.get_weather_name(name)
case name
when "fair" then "Fair Skies"
when "showers" then "Showers"
when "gales" then "Gales"
when "blizzards" then "Blizzards"
when "heat" then "Heat"
when "thunder" then "Thunderstorms"
when "gloom" then "Gloom"
when "snow" then "Snow"
when "fog" then "Fog"
when "umbral_wind" then "Umbral Wind"
else "Unknown Weather"
end
end
def self.get_seed
(Time.now.utc.to_i * 1000 / 1400000.0).floor
end
def self.hash(seed = get_seed)
base = (seed / 3).floor * 100 + ((seed + 1) % 3) * 8
# Ruby has no other convenient way to convert a signed integer into an unsigned one
step1 = [ ((base << 11) ^ base) ].pack("L").unpack("L").first
step2 = [ (([ (step1 >> 8) ].pack("L").unpack("L").first.to_i ^ step1)) ].pack("L").unpack("L").first.to_i
step2 % 100
end
def self.unsigned_right(input, by)
input << by & 0xFF00 | input >> by & 0xFF
end
end

BIN
public/weather/fair.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/weather/fog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/weather/showers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/weather/snow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/weather/thunder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB