diff --git a/.dockerignore b/.dockerignore
index ae77e8985c..f6cb39af04 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -9,4 +9,3 @@ project
target*
!target/build
-localDependencies/ajd4jp-8.0.2.2021.jar
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 63be34873e..bade594553 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
.idea/
*.iml
+# VSCode
+.vscode
+
# SBT
/project/target
/project/project
@@ -15,4 +18,10 @@ localDependencies/spigot-1.12.2.jar
# Docker
docker/spigot/serverfiles/eula.txt
-.env
\ No newline at end of file
+.env
+
+# Metal
+.bloop
+.metals
+project/.bloop/
+project/metals.sbt
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2b..a55e7a179b 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 579b344ff6..63076deff7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -69,7 +69,10 @@ $ docker compose up --build -d
最初に、[GiganticMinecraftのページ][gm-gh-repo]を開いて、画面右上にある「fork」と書かれた枝分かれしているアイコンがあるボタンを押します。
すると「Create a new fork」と書かれた画面に移動します。
-![img.png](img.png)
+
+
+![img.png](https://user-images.githubusercontent.com/127779256/226674317-3ad07000-a272-4f2e-905a-15e07b394bae.png)
+
いくつか入力欄がありますが、何も触らずにCreate forkを押します。
また画面が切り替わります。画面左上に書かれた文字が「GiganticMinecraft/SeichiAssist」ではなく、「(あなたのID)/SeichiAssist」になっていることを確認できたら次へ進みます。
@@ -163,11 +166,24 @@ DockerマシンのIPアドレス(Linux等なら`localhost`)を`DOCKER_IP`とし
`docker`により各サービスが起動したら、マルチプレイヤーのメニューで`DOCKER_IP`へと接続することができます。
また、`DOCKER_IP:8080`へとWebブラウザでアクセスすることで、phpMyAdminを介してデータベースを操作することができます。
-`/op`などのコマンドを実行するためにSpigotのコンソールにアクセスする必要がある場合、
-`spigota`または`spigotb`へのコンテナ名とともに `docker attach [CONTAINER_NAME]` を実行してください。
-コンテナ名は `docker ps` を実行すると `seichiassist_spigotb_1` のような形式で表示されます。
+##### コンソールにアクセスする
+
+自分のアカウントに管理者権限(OP)を与える時など、Spigotのコンソールにアクセスする場合は、
+`spigota` または `spigotb` のコンテナにアタッチする必要があります。
+
+アタッチするには `docker attach [CONTAINER_NAME]` を実行します。
+コンテナを指定する際に使用するIDはコマンドプロンプトで `docker ps` を実行すると `seichiassist_spigotb_1` のような形式で表示されます。
+
コンソールからは CtrlキーとCキーを同時押しすることで出ることができます。サーバーは停止されません。
+##### 整地ワールドの作成
+
+初めてデバッグ環境のSpigotに接続した際にスポーンするワールドは整地ワールドではないため、そのままブロックを破壊しても整地レベルは上昇しません。
+
+整地ワールドを作成する場合、OP権限を付与したプレイヤーかアタッチしたコンソールからコマンドで `mvcreate world_SW normal` を実行します。
+
+整地ワールドへ行くには、コマンドで `mvtp world_SW` を実行します。
+
### 反映する
さあ、いよいよ反映の時間がやってきました。
まずは手元で`sbt scalafixAll`をします。次に`sbt scalafmtAll`をします。
diff --git a/build.sbt b/build.sbt
index 699d81bbf4..ce496a0c94 100644
--- a/build.sbt
+++ b/build.sbt
@@ -8,7 +8,7 @@ import java.io._
ThisBuild / scalaVersion := "2.13.4"
// ThisBuild / version はGitHub Actionsによって取得/自動更新される。
// 次の行は ThisBuild / version := "(\d*)" の形式でなければならない。
-ThisBuild / version := "76"
+ThisBuild / version := "77"
ThisBuild / organization := "click.seichi"
ThisBuild / description := "ギガンティック☆整地鯖の独自要素を司るプラグイン"
@@ -50,7 +50,9 @@ resolvers ++= Seq(
"oss.sonatype.org" at "https://oss.sonatype.org/content/repositories/snapshots",
"nexus.okkero.com" at "https://nexus.okkero.com/repository/maven-releases/",
"maven.elmakers.com" at "https://maven.elmakers.com/repository/", // spigot-api 1.12.2がhub.spigotmc.orgからダウンロードできなくなったため
- "repo.phoenix616.dev" at "https://repo.phoenix616.dev" // authlibのための
+ "repo.phoenix616.dev" at "https://repo.phoenix616.dev", // authlibのための
+ // ajd4jpのミラーのため
+ "jitpack.io" at "https://jitpack.io"
)
val providedDependencies = Seq(
@@ -79,6 +81,7 @@ val dependenciesToEmbed = Seq(
"org.scala-lang.modules" %% "scala-collection-contrib" % "0.2.1",
// DB
+ "org.mariadb.jdbc" % "mariadb-java-client" % "3.1.4",
"org.flywaydb" % "flyway-core" % "5.2.4",
"org.scalikejdbc" %% "scalikejdbc" % "3.5.0",
@@ -111,6 +114,12 @@ val dependenciesToEmbed = Seq(
"io.circe" %% "circe-core" % "0.14.1",
"io.circe" %% "circe-generic" % "0.14.1",
"io.circe" %% "circe-parser" % "0.14.1",
+
+ // ajd4jp
+ "com.github.KisaragiEffective" % "ajd4jp-mirror" % "8.0.2.2021",
+
+ // Sentry
+ "io.sentry" % "sentry" % "6.18.1"
)
// endregion
@@ -127,6 +136,17 @@ assembly / assemblyExcludedJars := {
}
}
+// protocol配下とルートのLICENSEが衝突してCIが落ちる
+// cf. https://github.com/sbt/sbt-assembly/issues/141
+assembly / assemblyMergeStrategy := {
+ case PathList(ps @ _*) if ps.last endsWith "LICENSE" => MergeStrategy.rename
+ case PathList("org", "apache", "commons", "logging", xs @ _*) =>
+ MergeStrategy.last
+ case otherFile =>
+ val oldStrategy = (assembly / assemblyMergeStrategy).value
+ oldStrategy(otherFile)
+}
+
// endregion
// region プラグインJarに埋め込むリソースの処理
@@ -150,8 +170,6 @@ Compile / filteredResourceGenerator :=
Compile / resourceGenerators += (Compile / filteredResourceGenerator)
-Compile / unmanagedResources += baseDirectory.value / "LICENSE"
-
// トークン置換を行ったファイルをunmanagedResourcesのコピーから除外する
unmanagedResources / excludeFilter :=
filesToBeReplacedInResourceFolder.foldLeft((unmanagedResources / excludeFilter).value)(
@@ -186,7 +204,10 @@ lazy val root = (project in file(".")).settings(
"-Ymacro-annotations",
"-Ywarn-unused"
),
- javacOptions ++= Seq("-encoding", "utf8")
+ javacOptions ++= Seq("-encoding", "utf8"),
+ assembly / assemblyShadeRules ++= Seq(
+ ShadeRule.rename("org.mariadb.jdbc.**" -> "com.github.unchama.seichiassist.relocateddependencies.org.mariadb.jdbc.@1").inAll
+ )
)
// endregion
diff --git a/docker-compose.yml b/docker-compose.yml
index cb6587fe23..0a5683256f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -77,12 +77,11 @@ services:
- "25565:25577"
volumes:
- ./docker/bungeecord/serverfiles:/config
+ - ./docker/bungeecord/serverfiles/redisbungee/config.yml:/plugins/RedisBungee/config.yml
networks:
- seichi
environment:
- - REDIS_HOST=redis
- - REDIS_PORT=6379
- - SERVER_ID=bungeecord_1
+ - PLUGINS=https://github.com/ProxioDev/RedisBungee/releases/download/0.10.1/RedisBungee-Bungee-0.10.1-SNAPSHOT.jar
depends_on:
- spigota
- spigotb
diff --git a/docker/bungeecord/serverfiles/redisbungee/config.yml b/docker/bungeecord/serverfiles/redisbungee/config.yml
new file mode 100644
index 0000000000..e431463453
--- /dev/null
+++ b/docker/bungeecord/serverfiles/redisbungee/config.yml
@@ -0,0 +1,77 @@
+# RedisBungee configuration file.
+
+# Cluster Mode
+# enabling this option will enable cluster mode.
+cluster-mode-enabled: false
+
+# FORMAT:
+# redis-cluster-servers:
+# - host: 127.0.0.1
+# port: 2020
+# - host: 127.0.0.1
+# port: 2021
+# - host: 127.0.0.1
+# port: 2021
+
+# you can set single server and Jedis will automatically discover cluster nodes,
+# but might fail if this single redis node is down when Proxy startup, its recommended put the all the nodes
+redis-cluster-servers:
+ - host: 127.0.0.1
+ port: 6379
+
+# Get Redis from http://redis.io/
+# The Redis server you use.
+# these settings are ignored when cluster mode is enabled.
+redis-server: redis
+redis-port: 6379
+
+# OPTIONAL but recommended: If your Redis server uses AUTH, set the password required.
+redis-password: ""
+# Maximum connections that will be maintained to the Redis server.
+# The default is 10. This setting should be left as-is unless you have some wildly
+# inefficient plugins or a lot of players.
+max-redis-connections: 10
+
+# since redis can support ssl by version 6 you can use ssl / tls in redis bungee too!
+# but there is more configuration needed to work see https://github.com/ProxioDev/RedisBungee/issues/18
+# Keep note that SSL/TLS connections will decrease redis performance so use it when needed.
+useSSL: false
+
+# An identifier for this BungeeCord / Velocity instance. Will randomly generate if leaving it blank.
+proxy-id: ""
+
+# since version 0.8.0 Internally now uses JedisPooled instead of Jedis, JedisPool.
+# which will break compatibility with old plugins that uses RedisBungee JedisPool
+# so to mitigate this issue, we will instruct RedisBungee to init an JedisPool for compatibility reasons.
+# enabled by default
+# ignored when cluster mode is enabled
+enable-jedis-pool-compatibility: true
+# max connections for the compatibility pool
+compatibility-max-connections: 3
+
+# Register redis bungee legacy commands
+# if this disabled override-bungee-commands will be ignored
+register-legacy-commands: false
+
+# Whether or not RedisBungee should install its version of regular BungeeCord commands.
+# Often, the RedisBungee commands are desired, but in some cases someone may wish to
+# override the commands using another plugin.
+#
+# If you are just denying access to the commands, RedisBungee uses the default BungeeCord
+# permissions - just deny them and access will be denied.
+#
+# Please note that with build 787+, most commands overridden by RedisBungee were moved to
+# modules, and these must be disabled or overridden yourself.
+override-bungee-commands: false
+
+# A list of IP addresses for which RedisBungee will not modify the response for, useful for automatic
+# restart scripts.
+exempt-ip-addresses: []
+
+# restore old login when online behavior before 0.9.0 update
+# uncomment to enable it
+
+# disable-kick-when-online: true
+
+# Config version DO NOT CHANGE!!!!
+config-version: 1
\ No newline at end of file
diff --git a/localDependencies/ajd4jp-8.0.2.2021.jar b/localDependencies/ajd4jp-8.0.2.2021.jar
deleted file mode 100644
index 66b8b660af..0000000000
Binary files a/localDependencies/ajd4jp-8.0.2.2021.jar and /dev/null differ
diff --git a/project/scalapb.sbt b/project/scalapb.sbt
index a0a6ca4c91..8363432e9d 100644
--- a/project/scalapb.sbt
+++ b/project/scalapb.sbt
@@ -1,3 +1,3 @@
-addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.28")
+addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.6")
-libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.10.1"
+libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.11.11"
diff --git a/src/main/resources/db/migration/V1.16.0__Change_new_fairy_time_type.sql b/src/main/resources/db/migration/V1.16.0__Change_new_fairy_time_type.sql
new file mode 100644
index 0000000000..27f32025fc
--- /dev/null
+++ b/src/main/resources/db/migration/V1.16.0__Change_new_fairy_time_type.sql
@@ -0,0 +1,5 @@
+USE seichiassist;
+
+-- 型を変えるので一度内容をリセットする
+UPDATE playerdata SET newVotingFairyTime = NULL;
+ALTER TABLE playerdata MODIFY newVotingFairyTime DATETIME;
diff --git a/src/main/resources/db/migration/V1.16.1__Add_fairy_columns.sql b/src/main/resources/db/migration/V1.16.1__Add_fairy_columns.sql
new file mode 100644
index 0000000000..a069af9e8b
--- /dev/null
+++ b/src/main/resources/db/migration/V1.16.1__Add_fairy_columns.sql
@@ -0,0 +1,3 @@
+USE seichiassist;
+
+ALTER TABLE playerdata ADD is_fairy_speech_play_sound BOOLEAN DEFAULT TRUE AFTER toggleVotingFairy;
diff --git a/src/main/resources/db/migration/V1.16.2__Remove_duplicate_playerdata.sql b/src/main/resources/db/migration/V1.16.2__Remove_duplicate_playerdata.sql
new file mode 100644
index 0000000000..7d51ecbf95
--- /dev/null
+++ b/src/main/resources/db/migration/V1.16.2__Remove_duplicate_playerdata.sql
@@ -0,0 +1,14 @@
+-- 本来重複しないはずのuuidにunique制約をかけていなかった &
+-- それによって何故か全く同じデータをもつデータがplayerdataテーブルに存在しているのでそれを取り除く
+
+USE seichiassist;
+
+CREATE TEMPORARY TABLE playerdata_tmp AS SELECT DISTINCT * FROM playerdata;
+TRUNCATE TABLE playerdata;
+
+INSERT INTO playerdata SELECT * FROM playerdata_tmp;
+DROP TABLE playerdata_tmp;
+
+-- ゆるせないのでplayerdataテーブルのuuidカラムをprimary keyとして設定する
+
+ALTER TABLE playerdata ADD PRIMARY KEY(uuid)
diff --git a/src/main/resources/db/migration/V1.16.3__Move_vote_column.sql b/src/main/resources/db/migration/V1.16.3__Move_vote_column.sql
new file mode 100644
index 0000000000..ffe210598a
--- /dev/null
+++ b/src/main/resources/db/migration/V1.16.3__Move_vote_column.sql
@@ -0,0 +1,38 @@
+USE seichiassist;
+
+CREATE TABLE IF NOT EXISTS vote(
+ uuid CHAR(36) NOT NULL PRIMARY KEY,
+ vote_number INT NOT NULL,
+ chain_vote_number INT NOT NULL,
+ effect_point INT NOT NULL,
+ given_effect_point INT NOT NULL,
+ last_vote DATETIME
+);
+
+INSERT INTO
+ vote(
+ uuid,
+ vote_number,
+ chain_vote_number,
+ effect_point,
+ given_effect_point,
+ last_vote
+ )
+SELECT
+ uuid,
+ p_vote,
+ chainvote,
+ effectpoint,
+ p_givenvote,
+ CASE
+ WHEN lastvote REGEXP '[0-9]{4}/[0-9]{2}/[0-9]{2}' THEN CONVERT(lastvote, DATE)
+ END
+FROM
+ playerdata;
+
+ALTER TABLE
+ playerdata DROP IF EXISTS p_vote,
+ DROP IF EXISTS chainvote,
+ DROP IF EXISTS effectpoint,
+ DROP IF EXISTS p_givenvote,
+ DROP IF EXISTS lastvote;
diff --git a/src/main/resources/db/migration/V1.16.4__Move_vote_fairy_column.sql b/src/main/resources/db/migration/V1.16.4__Move_vote_fairy_column.sql
new file mode 100644
index 0000000000..44c7961983
--- /dev/null
+++ b/src/main/resources/db/migration/V1.16.4__Move_vote_fairy_column.sql
@@ -0,0 +1,44 @@
+USE seichiassist;
+
+CREATE TABLE vote_fairy(
+ uuid CHAR(36) NOT NULL PRIMARY KEY,
+ apple_open_state INT NOT NULL DEFAULT 1,
+ fairy_summon_cost INT NOT NULL DEFAULT 1,
+ is_fairy_using BOOLEAN NOT NULL DEFAULT false,
+ fairy_recovery_mana_value INT NOT NULL DEFAULT 0,
+ fairy_end_time DATETIME DEFAULT NULL,
+ given_apple_amount BIGINT NOT NULL DEFAULT 0,
+ is_play_fairy_speech_sound BOOLEAN NOT NULL DEFAULT true
+);
+
+INSERT INTO
+ vote_fairy(
+ uuid,
+ apple_open_state,
+ fairy_summon_cost,
+ is_fairy_using,
+ fairy_recovery_mana_value,
+ fairy_end_time,
+ given_apple_amount,
+ is_play_fairy_speech_sound
+ )
+SELECT
+ uuid,
+ toggleGiveApple,
+ toggleVotingFairy,
+ canVotingFairyUse,
+ VotingFairyRecoveryValue,
+ newVotingFairyTime,
+ p_apple,
+ is_fairy_speech_play_sound
+FROM
+ playerdata;
+
+ALTER TABLE playerdata
+ DROP IF EXISTS toggleGiveApple,
+ DROP IF EXISTS toggleVotingFairy,
+ DROP IF EXISTS canVotingFairyUse,
+ DROP IF EXISTS VotingFairyRecoveryValue,
+ DROP IF EXISTS newVotingFairyTime,
+ DROP IF EXISTS p_apple,
+ DROP IF EXISTS is_fairy_speech_play_sound
diff --git a/src/main/scala/com/github/unchama/bungeesemaphoreresponder/System.scala b/src/main/scala/com/github/unchama/bungeesemaphoreresponder/System.scala
index c6bbed2cb2..830a55e2ab 100644
--- a/src/main/scala/com/github/unchama/bungeesemaphoreresponder/System.scala
+++ b/src/main/scala/com/github/unchama/bungeesemaphoreresponder/System.scala
@@ -11,7 +11,7 @@ import org.bukkit.entity.Player
import org.bukkit.event.Listener
class System[F[_]: ConcurrentEffect: Timer](
- val finalizer: PlayerDataFinalizer[F, Player],
+ val finalizers: List[PlayerDataFinalizer[F, Player]],
messagePublishingContext: ContextShift[IO]
)(
implicit configuration: Configuration,
@@ -26,6 +26,6 @@ class System[F[_]: ConcurrentEffect: Timer](
implicit val _synchronization: BungeeSemaphoreSynchronization[F[Unit], PlayerName] = {
new RedisBungeeSemaphoreSynchronization[F]()
}
- Seq(new BungeeSemaphoreCooperator[F](finalizer))
+ Seq(new BungeeSemaphoreCooperator[F](finalizers))
}
}
diff --git a/src/main/scala/com/github/unchama/bungeesemaphoreresponder/bukkit/listeners/BungeeSemaphoreCooperator.scala b/src/main/scala/com/github/unchama/bungeesemaphoreresponder/bukkit/listeners/BungeeSemaphoreCooperator.scala
index d004ca451b..2c8d6d914d 100644
--- a/src/main/scala/com/github/unchama/bungeesemaphoreresponder/bukkit/listeners/BungeeSemaphoreCooperator.scala
+++ b/src/main/scala/com/github/unchama/bungeesemaphoreresponder/bukkit/listeners/BungeeSemaphoreCooperator.scala
@@ -1,11 +1,13 @@
package com.github.unchama.bungeesemaphoreresponder.bukkit.listeners
import cats.ApplicativeError
-import cats.effect.{Async, ConcurrentEffect, Timer}
+import cats.data.Validated
+import cats.effect.{Async, ConcurrentEffect, Sync, Timer}
import com.github.unchama.bungeesemaphoreresponder.Configuration
import com.github.unchama.bungeesemaphoreresponder.domain.actions.BungeeSemaphoreSynchronization
import com.github.unchama.bungeesemaphoreresponder.domain.{PlayerDataFinalizer, PlayerName}
-import com.github.unchama.generic.EitherExtra
+import com.github.unchama.generic.effect.ConcurrentExtra.attemptInParallel
+import com.github.unchama.generic.effect.MonadThrowExtra.retryUntilSucceeds
import com.github.unchama.generic.effect.unsafe.EffectEnvironment
import org.bukkit.entity.Player
import org.bukkit.event.player.PlayerQuitEvent
@@ -14,16 +16,13 @@ import org.bukkit.event.{EventHandler, EventPriority, Listener}
import scala.concurrent.duration.{Duration, FiniteDuration}
class BungeeSemaphoreCooperator[F[_]: ConcurrentEffect: Timer](
- finalizer: PlayerDataFinalizer[F, Player]
+ finalizers: List[PlayerDataFinalizer[F, Player]]
)(
implicit synchronization: BungeeSemaphoreSynchronization[F[Unit], PlayerName],
configuration: Configuration,
effectEnvironment: EffectEnvironment
) extends Listener {
- import cats.effect.implicits._
- import cats.implicits._
-
@EventHandler(priority = EventPriority.LOWEST)
def onQuit(event: PlayerQuitEvent): Unit = {
val player = event.getPlayer
@@ -36,17 +35,29 @@ class BungeeSemaphoreCooperator[F[_]: ConcurrentEffect: Timer](
case object TimeoutReached
extends Exception(s"Timeout ${configuration.saveTimeoutDuration} reached!")
+ val quitProcess = attemptInParallel(
+ finalizers.map(finalizer => retryUntilSucceeds(finalizer.onQuitOf(player))(10))
+ )
+
+ import cats.implicits._
+
val program = for {
- fiber <- finalizer.onQuitOf(player).attempt.start
- result <- ConcurrentEffect[F].race(timeout, fiber.join)
- _ <- EitherExtra.unassociate(result) match {
- case Left(timeoutOrErrorOnFinalization) =>
- synchronization.notifySaveFailureOf(name) >>
- ApplicativeError[F, Throwable].raiseError[Unit] {
- timeoutOrErrorOnFinalization.getOrElse(TimeoutReached)
- }
- case Right(_) =>
- synchronization.confirmSaveCompletionOf(name)
+ raceResult <- ConcurrentEffect[F].race(timeout, quitProcess)
+ _ <- raceResult match {
+ case Left(_) =>
+ synchronization.notifySaveFailureOf(name) >> ApplicativeError[F, Throwable]
+ .raiseError[Unit](TimeoutReached)
+ case Right(results) =>
+ results.traverse(e => Validated.fromEither(e).leftMap(List.apply(_))) match {
+ case Validated.Valid(_) =>
+ synchronization.confirmSaveCompletionOf(name)
+ case Validated.Invalid(errors) =>
+ synchronization.notifySaveFailureOf(name) >> errors.traverse(error =>
+ Sync[F].delay {
+ error.printStackTrace()
+ }
+ )
+ }
}
} yield ()
diff --git a/src/main/scala/com/github/unchama/bungeesemaphoreresponder/domain/PlayerDataFinalizer.scala b/src/main/scala/com/github/unchama/bungeesemaphoreresponder/domain/PlayerDataFinalizer.scala
index 45e6a72a3d..6ef72324c2 100644
--- a/src/main/scala/com/github/unchama/bungeesemaphoreresponder/domain/PlayerDataFinalizer.scala
+++ b/src/main/scala/com/github/unchama/bungeesemaphoreresponder/domain/PlayerDataFinalizer.scala
@@ -1,7 +1,6 @@
package com.github.unchama.bungeesemaphoreresponder.domain
-import cats.effect.ConcurrentEffect
-import cats.{Applicative, ApplicativeError, ~>}
+import cats.~>
import com.github.unchama.generic.ContextCoercion
/**
@@ -38,24 +37,4 @@ object PlayerDataFinalizer {
def apply[F[_], Player](f: Player => F[Unit]): PlayerDataFinalizer[F, Player] =
(player: Player) => f(player)
- import cats.effect.implicits._
- import cats.implicits._
-
- def concurrently[F[_]: ConcurrentEffect, Player](
- finalizers: List[PlayerDataFinalizer[F, Player]]
- ): PlayerDataFinalizer[F, Player] =
- PlayerDataFinalizer { player =>
- for {
- fibers <- finalizers.traverse(_.onQuitOf(player).attempt.start)
- results <- fibers.traverse(_.join)
- _ <-
- // TODO: 最初のエラーしか報告されていないが、全部報告すべき
- results.collectFirst { case Left(error) => error } match {
- case Some(error) =>
- ApplicativeError[F, Throwable].raiseError(error)
- case None =>
- Applicative[F].unit
- }
- } yield ()
- }
}
diff --git a/src/main/scala/com/github/unchama/generic/effect/ConcurrentExtra.scala b/src/main/scala/com/github/unchama/generic/effect/ConcurrentExtra.scala
index a7a5576702..a7b6ec4a4b 100644
--- a/src/main/scala/com/github/unchama/generic/effect/ConcurrentExtra.scala
+++ b/src/main/scala/com/github/unchama/generic/effect/ConcurrentExtra.scala
@@ -1,5 +1,6 @@
package com.github.unchama.generic.effect
+import cats.Applicative
import cats.effect.concurrent.Deferred
import cats.effect.{CancelToken, Concurrent}
@@ -33,4 +34,31 @@ object ConcurrentExtra {
a <- fiber.join
} yield a
+
+ import cats.effect._
+
+ /**
+ * 与えられた複数の入力プログラムをすべて並列に実行するようなプログラムを構築します。
+ * 構築されたプログラムは次のような挙動をします:
+ * - 構築されたプログラムの実行 fiber がキャンセルされた時、
+ * その時点で並列で走っている入力プログラムの実行 fiber がすべてキャンセルされます
+ * - 入力プログラムの実行 fiber がエラー等で早期終了しても、他の実行 fiber への影響はありません
+ * (一つの fiber が異常終了しても、他の fiber のキャンセルなどは行われません)
+ * - 結果の `List[Either[Throwable, A]]` は入力プログラムの終了結果を
+ * (入力プログラムが与えられた順と同じ順で) 保持しています。
+ *
+ * 各値は
+ * - `Right[A]` だった場合、入力プログラムが `A` を結果として正常終了したこと
+ * - `Left[Throwable]` だった場合、入力プログラムが例外を送出して異常終了したこと
+ * をそれぞれ表します。
+ */
+ def attemptInParallel[F[_]: ConcurrentEffect, A](
+ programs: List[F[A]]
+ ): F[List[Either[Throwable, A]]] = {
+ ConcurrentEffect[F]
+ .bracketCase(programs.traverse(Concurrent[F].start(_)))(_.traverse(_.join.attempt)) {
+ case (fibers, ExitCase.Canceled) => fibers.traverse(_.cancel).void
+ case _ => Applicative[F].unit
+ }
+ }
}
diff --git a/src/main/scala/com/github/unchama/generic/effect/MonadThrowExtra.scala b/src/main/scala/com/github/unchama/generic/effect/MonadThrowExtra.scala
new file mode 100644
index 0000000000..419b88733b
--- /dev/null
+++ b/src/main/scala/com/github/unchama/generic/effect/MonadThrowExtra.scala
@@ -0,0 +1,32 @@
+package com.github.unchama.generic.effect
+
+import cats.MonadError
+import cats.effect.Sync
+
+object MonadThrowExtra {
+
+ import cats.implicits._
+
+ def retryUntilSucceeds[F[_]: Sync, A](fa: F[A])(limit: Int): F[A] = {
+ require(limit >= 1)
+ def go(currentIterationCount: Int, occurredException: Option[Throwable]): F[A] = {
+ if (currentIterationCount <= limit) {
+ fa.attempt.flatMap {
+ case Right(a) =>
+ Sync[F].delay(occurredException.foreach(_.printStackTrace())).as(a)
+ case Left(error) =>
+ go(currentIterationCount + 1, Some(error))
+ }
+ } else {
+ // このelse節に入っている時点で1度は失敗しているので、`occurredExceptions`が`None`であることはありえない。
+ case object LimitReached
+ extends Exception(s"Limit $limit reached!", occurredException.last)
+
+ MonadError[F, Throwable].raiseError(LimitReached)
+ }
+ }
+
+ go(0, None)
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/AntiTypesafe.java b/src/main/scala/com/github/unchama/seichiassist/AntiTypesafe.java
deleted file mode 100644
index 2bb7fe25d2..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/AntiTypesafe.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.github.unchama.seichiassist;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target({ElementType.METHOD, ElementType.FIELD})
-@Retention(RetentionPolicy.SOURCE)
-public @interface AntiTypesafe {
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
index 121729670e..63fd303166 100644
--- a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
@@ -39,7 +39,6 @@ import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.{
asyncShift,
onMainThread
}
-import com.github.unchama.seichiassist.data.RankData
import com.github.unchama.seichiassist.data.player.PlayerData
import com.github.unchama.seichiassist.database.DatabaseGateway
import com.github.unchama.seichiassist.domain.actions.{
@@ -85,11 +84,16 @@ import com.github.unchama.seichiassist.subsystems.minestack.bukkit.MineStackComm
import com.github.unchama.seichiassist.subsystems.present.infrastructure.GlobalPlayerAccessor
import com.github.unchama.seichiassist.subsystems.seasonalevents.api.SeasonalEventsAPI
import com.github.unchama.seichiassist.subsystems.sharedinventory.SharedInventoryAPI
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.FairyAPI
import com.github.unchama.seichiassist.subsystems.tradesystems.subsystems.gttosiina.GtToSiinaAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.FairySpeechAPI
import com.github.unchama.seichiassist.task.PlayerDataSaveTask
import com.github.unchama.seichiassist.task.global._
import com.github.unchama.util.{ActionStatus, ClassUtils}
import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
+import io.sentry.Sentry
+import io.sentry.SentryLevel
import org.bukkit.Bukkit
import org.bukkit.ChatColor._
import org.bukkit.entity.{Entity, Player, Projectile}
@@ -451,6 +455,31 @@ class SeichiAssist extends JavaPlugin() {
subsystems.idletime.subsystems.awayscreenname.System.wired[IO].unsafeRunSync()
}
+ // TODO: これはprivateであるべきだが、Achievementシステムが再実装されるまでやむを得ずpublicにする
+ lazy val voteSystem: subsystems.vote.System[IO, Player] = {
+ implicit val breakCountAPI: BreakCountAPI[IO, SyncIO, Player] = breakCountSystem.api
+
+ subsystems.vote.System.wired[IO, SyncIO]
+ }
+
+ private lazy val fairySystem: subsystems.vote.subsystems.fairy.System[IO, SyncIO, Player] = {
+ import PluginExecutionContexts.{asyncShift, sleepAndRoutineContext}
+ implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect(asyncShift)
+ implicit val breakCountAPI: BreakCountAPI[IO, SyncIO, Player] = breakCountSystem.api
+ implicit val voteAPI: VoteAPI[IO, Player] = voteSystem.api
+ implicit val manaApi: ManaApi[IO, SyncIO, Player] = manaSystem.manaApi
+ implicit val fairySpeechAPI: FairySpeechAPI[IO, Player] = fairySpeechSystem.api
+
+ subsystems.vote.subsystems.fairy.System.wired.unsafeRunSync()
+ }
+
+ private lazy val fairySpeechSystem
+ : subsystems.vote.subsystems.fairyspeech.System[IO, Player] = {
+ import PluginExecutionContexts.timer
+
+ subsystems.vote.subsystems.fairyspeech.System.wired[IO]
+ }
+
/* TODO: mineStackSystemは本来privateであるべきだが、mineStackにアイテムを格納するAPIが現状の
BreakUtilの実装から呼び出されている都合上やむを得ずpublicになっている。*/
lazy val mineStackSystem: subsystems.minestack.System[IO, Player, ItemStack] =
@@ -476,6 +505,9 @@ class SeichiAssist extends JavaPlugin() {
homeSystem,
presentSystem,
anywhereEnderSystem,
+ voteSystem,
+ fairySpeechSystem,
+ fairySystem,
gachaPrizeSystem,
idleTimeSystem,
awayScreenNameSystem,
@@ -516,15 +548,13 @@ class SeichiAssist extends JavaPlugin() {
import PluginExecutionContexts.timer
new BungeeSemaphoreResponderSystem(
- PlayerDataFinalizer.concurrently[IO, Player](
- Seq(
- savePlayerData,
- assaultSkillRoutinesRepositoryControls.finalizer.coerceContextTo[IO],
- activeSkillAvailabilityRepositoryControls.finalizer.coerceContextTo[IO]
- ).appendedAll(wiredSubsystems.flatMap(_.managedFinalizers))
- .appendedAll(wiredSubsystems.flatMap(_.managedRepositoryControls.map(_.finalizer)))
- .toList
- ),
+ Seq(
+ savePlayerData,
+ assaultSkillRoutinesRepositoryControls.finalizer.coerceContextTo[IO],
+ activeSkillAvailabilityRepositoryControls.finalizer.coerceContextTo[IO]
+ ).appendedAll(wiredSubsystems.flatMap(_.managedFinalizers))
+ .appendedAll(wiredSubsystems.flatMap(_.managedRepositoryControls.map(_.finalizer)))
+ .toList,
PluginExecutionContexts.asyncShift
)
}
@@ -566,6 +596,20 @@ class SeichiAssist extends JavaPlugin() {
// コンフィグ系の設定は全てConfig.javaに移動
SeichiAssist.seichiAssistConfig = Config.loadFrom(this)
+ Sentry.init { options =>
+ options.setDsn("https://7f241763b17c49db982ea29ad64b0264@sentry.onp.admin.seichi.click/2")
+ // パフォーマンスモニタリングに使うトレースサンプルの送信割合
+ // tracesSampleRateを1.0にすると全てのイベントが送られるため、送りすぎないように調整する必要がある
+ options.setTracesSampleRate(0.25)
+ // When first trying Sentry it's good to see what the SDK is doing:
+ options.setDebug(true)
+
+ // どのサーバーからイベントが送られているのかを判別する識別子
+ options.setEnvironment(SeichiAssist.seichiAssistConfig.getServerId)
+ }
+
+ Sentry.configureScope(_.setLevel(SentryLevel.WARNING))
+
if (SeichiAssist.seichiAssistConfig.getDebugMode == 1) {
// debugmode=1の時は最初からデバッグモードで鯖を起動
logger.info(s"${RED}SeichiAssistをデバッグモードで起動します")
@@ -638,6 +682,9 @@ class SeichiAssist extends JavaPlugin() {
anywhereEnderSystem.accessApi
implicit val sharedInventoryAPI: SharedInventoryAPI[IO, Player] =
sharedInventorySystem.api
+ implicit val voteAPI: VoteAPI[IO, Player] = voteSystem.api
+ implicit val fairyAPI: FairyAPI[IO, SyncIO, Player] = fairySystem.api
+ implicit val fairySpeechAPI: FairySpeechAPI[IO, Player] = fairySpeechSystem.api
implicit val donateAPI: DonatePremiumPointAPI[IO] = donateSystem.api
implicit val gachaTicketAPI: GachaTicketAPI[IO] =
gachaTicketSystem.api
@@ -662,7 +709,6 @@ class SeichiAssist extends JavaPlugin() {
// コマンドの登録
Map(
- "vote" -> VoteCommand.executor,
"map" -> MapCommand.executor,
"ef" -> new EffectCommand(fastDiggingEffectSystem.settingsApi).executor,
"seichiassist" -> SeichiAssistCommand.executor,
@@ -679,7 +725,7 @@ class SeichiAssist extends JavaPlugin() {
case (commandName, executor) => getCommand(commandName).setExecutor(executor)
}
- import menuRouter.canOpenAchievementMenu
+ import menuRouter.ioCanOpenNickNameMenu
// リスナーの登録
val listeners = Seq(
new PlayerJoinListener(),
@@ -709,11 +755,6 @@ class SeichiAssist extends JavaPlugin() {
getServer.getPluginManager.registerEvents(_, this)
}
- // ランキングリストを最新情報に更新する
- if (!SeichiAssist.databaseGateway.playerDataManipulator.successRankingUpdate()) {
- throw new RuntimeException("ランキングデータの作成に失敗しました。サーバーを停止します…")
- }
-
startRepeatedJobs()
// サブシステムのリポジトリのバックアップ処理を走らせる
@@ -764,7 +805,6 @@ class SeichiAssist extends JavaPlugin() {
val startTask = {
val dataRecalculationRoutine = {
import PluginExecutionContexts._
- implicit val manaApi: ManaApi[IO, SyncIO, Player] = manaSystem.manaApi
PlayerDataRecalculationRoutine()
}
@@ -842,7 +882,9 @@ class SeichiAssist extends JavaPlugin() {
.getOnlinePlayers
.asScala
.toList
- .traverse(bungeeSemaphoreResponderSystem.finalizer.onQuitOf)
+ .flatTraverse { player =>
+ bungeeSemaphoreResponderSystem.finalizers.traverse(_.onQuitOf(player))
+ }
.unsafeRunSync()
if (SeichiAssist.databaseGateway.disconnect() == ActionStatus.Fail) {
@@ -868,12 +910,6 @@ class SeichiAssist extends JavaPlugin() {
object SeichiAssist {
// Playerdataに依存するデータリスト
val playermap: mutable.HashMap[UUID, PlayerData] = mutable.HashMap()
- // プレイ時間ランキング表示用データリスト
- val ranklist_playtick: mutable.ArrayBuffer[RankData] = mutable.ArrayBuffer()
- // 投票ポイント表示用データリスト
- val ranklist_p_vote: mutable.ArrayBuffer[RankData] = mutable.ArrayBuffer()
- // マナ妖精表示用のデータリスト
- val ranklist_p_apple: mutable.ArrayBuffer[RankData] = mutable.ArrayBuffer()
var instance: SeichiAssist = _
// デバッグフラグ(デバッグモード使用時はここで変更するのではなくconfig.ymlの設定値を変更すること!)
@@ -882,7 +918,6 @@ object SeichiAssist {
// TODO staticであるべきではない
var databaseGateway: DatabaseGateway = _
var seichiAssistConfig: Config = _
- var allplayergiveapplelong = 0L
object Scopes {
implicit val globalChatInterceptionScope: InterceptionScope[UUID, String] = {
diff --git a/src/main/scala/com/github/unchama/seichiassist/achievement/AchievementConditions.scala b/src/main/scala/com/github/unchama/seichiassist/achievement/AchievementConditions.scala
index 7238c5dd55..4a7f0cf32e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/achievement/AchievementConditions.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/achievement/AchievementConditions.scala
@@ -112,7 +112,9 @@ object AchievementConditions {
}
def voteCount_>=(n: Int): AchievementCondition[String] = {
- val predicate = playerDataPredicate(d => IO { d.p_vote_forT >= n })
+ val predicate = playerDataPredicate(d =>
+ IO { SeichiAssist.instance.voteSystem.api.count(d.uuid).unsafeRunSync().value >= n }
+ )
AchievementCondition(predicate, "JMS投票数が " + _ + " を超える", n.toString)
}
diff --git a/src/main/scala/com/github/unchama/seichiassist/commands/RmpCommand.scala b/src/main/scala/com/github/unchama/seichiassist/commands/RmpCommand.scala
index b2ac950f93..05d57d7de7 100644
--- a/src/main/scala/com/github/unchama/seichiassist/commands/RmpCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/commands/RmpCommand.scala
@@ -28,9 +28,6 @@ object RmpCommand {
s"$RED/rmp remove [world名] [日数]",
"全Ownerが[日数]間ログインしていないRegionを削除します(整地ワールドのみ)",
"",
- s"$RED/rmp removeAll [world名]",
- "原則全てのRegionを削除します(整地ワールドのみ)",
- "",
s"$RED/rmp list [world名] [日数]",
"全Ownerが[日数]間ログインしていないRegionを表示します"
)
@@ -59,16 +56,6 @@ object RmpCommand {
}
.build()
- private val removeAllExecutor = argsAndSenderConfiguredBuilder
- .execution { context =>
- val world = context.args.parsed.head.asInstanceOf[World]
- // -1を指定することで実質的に原則すべての保護を削除することになる
- val days = -1
-
- removeRegions(world, days)
- }
- .build()
-
private val listExecutor = argsAndSenderConfiguredBuilder
.execution { context =>
val world = context.args.parsed.head.asInstanceOf[World]
@@ -92,10 +79,8 @@ object RmpCommand {
val isSeichiWorldWithWGRegionsOption =
ManagedWorld.fromBukkitWorld(world).map(_.isSeichiWorldWithWGRegions)
- val commandName = if (days == -1) "removeAll" else "remove"
-
isSeichiWorldWithWGRegionsOption match {
- case None | Some(false) => MessageEffect(s"${commandName}コマンドは保護をかけて整地する整地ワールドでのみ使用出来ます")
+ case None | Some(false) => MessageEffect(s"第1整地以外の保護をかけて整地する整地ワールドでのみ使用出来ます")
case Some(true) =>
getOldRegionsIn(world, days).map { removalTargets =>
removalTargets.foreach { target =>
@@ -144,7 +129,7 @@ object RmpCommand {
val executor: TabExecutor =
BranchedExecutor(
- Map("remove" -> removeExecutor, "removeAll" -> removeAllExecutor, "list" -> listExecutor),
+ Map("remove" -> removeExecutor, "list" -> listExecutor),
whenArgInsufficient = Some(printDescriptionExecutor),
whenBranchNotFound = Some(printDescriptionExecutor)
).asNonBlockingTabExecutor()
diff --git a/src/main/scala/com/github/unchama/seichiassist/commands/VoteCommand.scala b/src/main/scala/com/github/unchama/seichiassist/commands/VoteCommand.scala
deleted file mode 100644
index 4d9eddb4dc..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/commands/VoteCommand.scala
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.github.unchama.seichiassist.commands
-
-import com.github.unchama.contextualexecutor.builder.ContextualExecutorBuilder
-import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
-import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.targetedeffect.commandsender.MessageEffect
-import com.github.unchama.targetedeffect.{SequentialEffect, UnfocusedEffect}
-import org.bukkit.ChatColor.{RED, YELLOW}
-import org.bukkit.command.TabExecutor
-
-object VoteCommand {
- private val usageEchoExecutor: EchoExecutor = EchoExecutor(
- MessageEffect(List(s"$RED/vote record <プレイヤー名>", "投票特典配布用コマンドです"))
- )
-
- private val recordExecutor =
- ContextualExecutorBuilder
- .beginConfiguration()
- .executionCSEffect(context => {
- val playerName: String = context.args.yetToBeParsed.head
- val lowerCasePlayerName = playerName.toLowerCase
-
- SequentialEffect(
- MessageEffect(s"$YELLOW${lowerCasePlayerName}の投票特典配布処理開始…"),
- UnfocusedEffect {
- SeichiAssist
- .databaseGateway
- .playerDataManipulator
- .incrementVotePoint(lowerCasePlayerName)
- },
- UnfocusedEffect {
- SeichiAssist.databaseGateway.playerDataManipulator.addChainVote(lowerCasePlayerName)
- }
- )
- })
- .build()
-
- val executor: TabExecutor = {
- BranchedExecutor(
- Map("record" -> recordExecutor),
- whenBranchNotFound = Some(usageEchoExecutor),
- whenArgInsufficient = Some(usageEchoExecutor)
- ).asNonBlockingTabExecutor()
- }
-}
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/MenuInventoryData.java b/src/main/scala/com/github/unchama/seichiassist/data/MenuInventoryData.java
index 8bd6a40f97..f5a75a1d6c 100644
--- a/src/main/scala/com/github/unchama/seichiassist/data/MenuInventoryData.java
+++ b/src/main/scala/com/github/unchama/seichiassist/data/MenuInventoryData.java
@@ -4,11 +4,8 @@
import com.github.unchama.seichiassist.achievement.Nicknames;
import com.github.unchama.seichiassist.data.player.AchievementPoint;
import com.github.unchama.seichiassist.data.player.PlayerData;
-import com.github.unchama.seichiassist.data.player.PlayerNickname;
-import com.github.unchama.seichiassist.task.VotingFairyTask;
import com.github.unchama.seichiassist.util.AsyncInventorySetter;
import com.github.unchama.seichiassist.util.ItemMetaFactory;
-import com.github.unchama.seichiassist.util.TypeConverter;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
@@ -24,7 +21,10 @@
import scala.collection.mutable.HashMap;
import scala.collection.mutable.Map;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
import java.util.function.Consumer;
public final class MenuInventoryData {
@@ -44,31 +44,6 @@ private MenuInventoryData() {
private static final Map shopIndex = new HashMap<>(60, 0.75);
private static final Map taihiIndex = new HashMap<>(60, 0.75);
- private static final List> loreTable = Arrays.asList(
- Collections.emptyList(),
- Arrays.asList(
- ChatColor.RED + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "ガンガンたべるぞ",
- ChatColor.RESET + "" + ChatColor.GRAY + "とにかく妖精さんにりんごを開放します。",
- ChatColor.RESET + "" + ChatColor.GRAY + "めっちゃ喜ばれます。"
- ),
- Arrays.asList(
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "バッチリたべよう",
- ChatColor.RESET + "" + ChatColor.GRAY + "食べ過ぎないように注意しつつ",
- ChatColor.RESET + "" + ChatColor.GRAY + "妖精さんにりんごを開放します。",
- ChatColor.RESET + "" + ChatColor.GRAY + "喜ばれます。"
- ),
- Arrays.asList(
- ChatColor.GREEN + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "リンゴだいじに",
- ChatColor.RESET + "" + ChatColor.GRAY + "少しだけ妖精さんにりんごを開放します。",
- ChatColor.RESET + "" + ChatColor.GRAY + "伝えると大抵落ち込みます。"
- ),
- Arrays.asList(
- ChatColor.BLUE + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "リンゴつかうな",
- ChatColor.RESET + "" + ChatColor.GRAY + "絶対にりんごを開放しません。",
- ChatColor.RESET + "" + ChatColor.GRAY + ""
- )
- );
-
/**
* (short) 3はダサいし、マジックコンスタントみたいだよね。
*/
@@ -80,8 +55,6 @@ private MenuInventoryData() {
*/
private static final Function0 FALSE = () -> false;
- private static final Consumer DIG100 = meta -> meta.addEnchant(Enchantment.DIG_SPEED, 100, false);
-
private static final ItemStack toMoveNicknameMenu = build(
Material.BARRIER,
@@ -89,138 +62,6 @@ private MenuInventoryData() {
ChatColor.RESET + "" + ChatColor.DARK_RED + "" + ChatColor.UNDERLINE + "クリックで移動"
);
- //投票特典受け取りボタン
- private static List getVoteButtonLore(final PlayerData playerdata) {
- return Arrays.asList(
- ChatColor.RESET + "" + ChatColor.GRAY + "投票特典を受け取るには",
- ChatColor.RESET + "" + ChatColor.GRAY + "投票ページで投票した後",
- ChatColor.RESET + "" + ChatColor.GRAY + "このボタンをクリックします",
- ChatColor.RESET + "" + ChatColor.AQUA + "特典受取済投票回数:" + playerdata.p_givenvote(),
- ChatColor.RESET + "" + ChatColor.AQUA + "特典未受取投票回数:" + (playerdata.p_vote_forT() - playerdata.p_givenvote()),
- ChatColor.RESET + "" + ChatColor.AQUA + "所有投票pt:" + playerdata.effectPoint()
- );
- }
-
- /**
- * 二つ名組み合わせ
- * @param p プレイヤー
- * @return メニュー
- */
- public static Inventory computeRefreshedCombineMenu(final Player p) {
- final UUID uuid = p.getUniqueId();
- final PlayerData playerdata = SeichiAssist.playermap().apply(uuid);
- //念のためエラー分岐
- if (isError(p, playerdata, "二つ名組み合わせ")) return null;
- final Inventory inventory = getEmptyInventory(4, MenuType.COMBINE.invName);
-
- //各ボタンの設定
- finishedHeadPageBuild.put(uuid, false);
- finishedMiddlePageBuild.put(uuid, false);
- finishedTailPageBuild.put(uuid, false);
- finishedShopPageBuild.put(uuid, false);
- headPartIndex.put(uuid, 0);
- middlePartIndex.put(uuid, 0);
- tailPartIndex.put(uuid, 0);
- shopIndex.put(uuid, 0);
- taihiIndex.put(uuid, 0);
-
- //実績ポイントの最新情報反映ボタン
- {
- // dynamic button
- final List lore = Arrays.asList(
- ChatColor.RESET + "" + ChatColor.GREEN + "クリックで情報を最新化",
- ChatColor.RESET + "" + ChatColor.RED + "累計獲得量:" + playerdata.achievePoint().cumulativeTotal(),
- ChatColor.RESET + "" + ChatColor.RED + "累計消費量:" + playerdata.achievePoint().used(),
- ChatColor.RESET + "" + ChatColor.AQUA + "使用可能量:" + playerdata.achievePoint().left()
- );
-
- final ItemStack itemstack = build(
- Material.EMERALD_ORE,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "実績ポイント 情報",
- lore
- );
-
- AsyncInventorySetter.setItemAsync(inventory, 0, itemstack);
- }
- //パーツショップ
- {
- // const button
- final ItemStack itemstack = build(
- Material.ITEM_FRAME,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "実績ポイントショップ",
- ChatColor.RESET + "" + ChatColor.GREEN + "クリックで開きます"
- );
- AsyncInventorySetter.setItemAsync(inventory, 9, itemstack);
- }
- //エフェクトポイントからの変換ボタン
- {
- // dynamic button
- final List lore = Arrays.asList(ChatColor.RESET + "" + ChatColor.RED + "JMS投票で手に入るポイントを",
- ChatColor.RESET + "" + ChatColor.RED + "実績ポイントに変換できます。",
- ChatColor.RESET + "" + ChatColor.YELLOW + "" + ChatColor.BOLD + "投票pt 10pt → 実績pt 3pt",
- ChatColor.RESET + "" + ChatColor.AQUA + "クリックで変換を一回行います。",
- ChatColor.RESET + "" + ChatColor.GREEN + "所有投票pt :" + playerdata.effectPoint(),
- ChatColor.RESET + "" + ChatColor.GREEN + "所有実績pt :" + playerdata.achievePoint().left()
- );
-
- final ItemStack itemstack = build(
- Material.EMERALD,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "ポイント変換ボタン",
- lore
- );
- AsyncInventorySetter.setItemAsync(inventory, 1, itemstack);
- }
-
- {
- final PlayerNickname nickname = playerdata.settings().nickname();
- final String playerTitle = Nicknames.getTitleFor(nickname.id1(), nickname.id2(), nickname.id3());
- final ItemStack itemStack = build(
- Material.BOOK,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "現在の二つ名の確認",
- ChatColor.RESET + "" + ChatColor.RED + "「" + playerTitle + "」"
- );
- AsyncInventorySetter.setItemAsync(inventory, 4, itemStack);
- }
-
- {
- // const button
- final ItemStack toHeadSelection = build(
- Material.WATER_BUCKET,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "前パーツ選択画面",
- ChatColor.RESET + "" + ChatColor.RED + "クリックで移動します"
- );
-
- // const button
- final ItemStack toMiddleSelection = build(
- Material.MILK_BUCKET,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "中パーツ選択画面",
- ChatColor.RESET + "" + ChatColor.RED + "クリックで移動します"
- );
-
- // const button
- final ItemStack toTailSelection = build(
- Material.LAVA_BUCKET,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "後パーツ選択画面",
- ChatColor.RESET + "" + ChatColor.RED + "クリックで移動します"
- );
- AsyncInventorySetter.setItemAsync(inventory, 11, toHeadSelection);
- AsyncInventorySetter.setItemAsync(inventory, 13, toMiddleSelection);
- AsyncInventorySetter.setItemAsync(inventory, 15, toTailSelection);
- }
-
- // 1ページ目を開く
- {
- // const Button
- final ItemStack itemstack = buildPlayerSkull(
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "実績・二つ名メニューへ",
- ChatColor.RESET + "" + ChatColor.DARK_RED + "" + ChatColor.UNDERLINE + "クリックで移動",
- "MHF_ArrowLeft"
- );
- AsyncInventorySetter.setItemAsync(inventory, 27, itemstack.clone());
- }
- return inventory;
- }
-
public enum MenuType {
HEAD("" + ChatColor.DARK_PURPLE + ChatColor.BOLD + "二つ名組合せ「前」"),
MIDDLE("" + ChatColor.DARK_PURPLE + ChatColor.BOLD + "二つ名組合せ「中」"),
@@ -594,224 +435,6 @@ public static Inventory computePartsShopMenu(final Player p) {
return inventory;
}
- /**
- * 投票妖精メニュー
- * @param p プレイヤー
- * @return メニュー
- */
- public static Inventory getVotingMenuData(final Player p) {
- //UUID取得
- final UUID uuid = p.getUniqueId();
- //プレイヤーデータ
- final PlayerData playerdata = SeichiAssist.playermap().apply(uuid);
- //念のためエラー分岐
- if (isError(p, playerdata, "投票妖精")) return null;
- final Inventory inventory = getEmptyInventory(4, ChatColor.DARK_PURPLE + "" + ChatColor.BOLD + "投票ptメニュー");
-
- //投票pt受け取り
- {
- // dynamic button
- final ItemStack itemstack = build(
- Material.DIAMOND,
- ChatColor.LIGHT_PURPLE + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "クリックで投票特典を受け取れます",
- getVoteButtonLore(playerdata),
- DIG100
- );
- AsyncInventorySetter.setItemAsync(inventory, 0, itemstack);
- }
-
- // ver0.3.2 投票ページ表示
- // const button
- {
- final List lore = Arrays.asList(ChatColor.RESET + "" + ChatColor.GREEN + "投票すると様々な特典が!",
- ChatColor.RESET + "" + ChatColor.GREEN + "1日1回投票出来ます",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "クリックするとチャット欄に",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "URLが表示されますので",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "Tキーを押してから",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "そのURLをクリックしてください"
- );
-
- final ItemStack itemstack = build(
- Material.BOOK_AND_QUILL,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "投票ページにアクセス",
- lore
- );
- AsyncInventorySetter.setItemAsync(inventory, 9, itemstack);
- }
-
- //棒メニューに戻る
- {
- // const button
- final List lore = Collections.singletonList(ChatColor.RESET + "" + ChatColor.DARK_RED + "" + ChatColor.UNDERLINE + "クリックで移動");
- final ItemStack itemstack = buildPlayerSkull(ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "ホームへ", lore, "MHF_ArrowLeft");
- AsyncInventorySetter.setItemAsync(inventory, 27, itemstack.clone());
- }
-
- //妖精召喚時間設定トグルボタン
- {
- // const button
- final List list = Arrays.asList(
- ChatColor.RESET + "" + ChatColor.GREEN + "" + ChatColor.BOLD + "" + VotingFairyTask.dispToggleVFTime(playerdata.toggleVotingFairy()),
- "",
- ChatColor.RESET + "" + ChatColor.GRAY + "コスト",
- ChatColor.RESET + "" + ChatColor.RED + "" + ChatColor.BOLD + "" + playerdata.toggleVotingFairy() * 2 + "投票pt",
- "",
- ChatColor.RESET + "" + ChatColor.DARK_RED + "" + ChatColor.UNDERLINE + "クリックで切替"
- );
-
- final ItemStack itemStack = build(
- Material.WATCH,
- ChatColor.AQUA + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "マナ妖精 時間設定",
- list
- );
-
- AsyncInventorySetter.setItemAsync(inventory, 2, itemStack);
- }
-
- //妖精契約設定トグル
- {
- final ItemStack itemStack = new ItemStack(Material.PAPER);
- itemStack.setItemMeta(getVotingFairyContractMeta(playerdata));
- AsyncInventorySetter.setItemAsync(inventory, 11, itemStack);
- }
- //妖精音トグル
- {
- final ItemStack itemStack = new ItemStack(Material.JUKEBOX);
- itemStack.setItemMeta(getVotingFairySoundsToggleMeta(playerdata.toggleVFSound()));
- AsyncInventorySetter.setItemAsync(inventory, 20, itemStack);
- }
-
- //妖精召喚
- {
- final List lore = Arrays.asList(
- ChatColor.RESET + "" + ChatColor.GRAY + "" + playerdata.toggleVotingFairy() * 2 + "投票ptを消費して",
- ChatColor.RESET + "" + ChatColor.GRAY + "マナ妖精を呼びます",
- ChatColor.RESET + "" + ChatColor.GRAY + "時間 : " + VotingFairyTask.dispToggleVFTime(playerdata.toggleVotingFairy()),
- ChatColor.RESET + "" + ChatColor.DARK_RED + "Lv.10以上で解放"
- );
-
- // dynamic button
- final ItemStack itemStack = build(
- Material.GHAST_TEAR,
- ChatColor.LIGHT_PURPLE + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "マナ妖精 召喚",
- lore,
- DIG100
- );
-
- AsyncInventorySetter.setItemAsync(inventory, 4, itemStack);
- }
-
-
- if (playerdata.usingVotingFairy()) {
- //妖精 時間確認
- {
- // const button
- final List lore = Arrays.asList(
- ChatColor.RESET + "" + ChatColor.GRAY + "妖精さんはいそがしい。",
- ChatColor.GRAY + "帰っちゃう時間を教えてくれる"
- );
-
- final ItemStack itemStack = build(
- Material.COMPASS,
- ChatColor.LIGHT_PURPLE + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "マナ妖精に時間を聞く",
- lore,
- DIG100
- );
-
- AsyncInventorySetter.setItemAsync(inventory, 13, itemStack);
- }
-
- {
- // dynamic button
- final int yourRank = playerdata.calcPlayerApple();
- final List lore = new ArrayList<>(6 + 2 * 4 + 5);
-
- // 6
- lore.addAll(Arrays.asList(
- ChatColor.RESET + "" + ChatColor.RED + "" + ChatColor.BOLD + "※ニンゲンに見られないように気を付けること!",
- ChatColor.RESET + "" + ChatColor.RED + "" + ChatColor.BOLD + " 毎日大妖精からデータを更新すること!",
- "",
- ChatColor.RESET + "" + ChatColor.GOLD + "" + ChatColor.BOLD + "昨日までにがちゃりんごを",
- ChatColor.RESET + "" + ChatColor.GOLD + "" + ChatColor.BOLD + "たくさんくれたニンゲンたち",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "召喚されたらラッキーだよ!"
- ));
- for (int rank = 0; rank <= 3; rank++) {
- if (rank >= SeichiAssist.ranklist_p_apple().size()) {
- break;
- }
- final RankData rankdata = SeichiAssist.ranklist_p_apple().apply(rank);
- if (rankdata.p_apple == 0) {
- break;
- }
- // 2 x 4 = 8
- lore.add(ChatColor.GRAY + "たくさんくれたニンゲン第" + (rank + 1) + "位!");
- lore.add(ChatColor.GRAY + "なまえ:" + rankdata.name + " りんご:" + rankdata.p_apple + "個");
- }
-
- // 5
- lore.add(ChatColor.AQUA + "ぜーんぶで" + SeichiAssist.allplayergiveapplelong() + "個もらえた!");
- lore.add("");
- lore.add(ChatColor.GREEN + "↓呼び出したニンゲンの情報↓");
- lore.add(ChatColor.GREEN + "今までに" + playerdata.p_apple() + "個もらった");
- lore.add(ChatColor.GREEN + "ニンゲンの中では" + yourRank + "番目にたくさんくれる!");
-
- final ItemStack itemStack = build(
- Material.GOLDEN_APPLE,
- ChatColor.YELLOW + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "㊙ がちゃりんご情報 ㊙",
- lore,
- DIG100
- );
- AsyncInventorySetter.setItemAsync(inventory, 6, itemStack);
- }
- }
-
- return inventory;
- }
-
- /**
- * 投票妖精音切り替え
- * @param playSound trueなら鳴らす
- * @return ラベルがついたアイテム
- */
- private static ItemMeta getVotingFairySoundsToggleMeta(final boolean playSound) {
- final ItemMeta itemmeta = Bukkit.getItemFactory().getItemMeta(Material.JUKEBOX);
- final List lore;
- itemmeta.setDisplayName(ChatColor.GOLD + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "マナ妖精の音トグル");
- if (playSound) {
- lore = Arrays.asList(
- ChatColor.RESET + "" + ChatColor.GREEN + "現在音が鳴る設定になっています。",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "※この機能はデフォルトでONです。",
- ChatColor.RESET + "" + ChatColor.DARK_RED + "" + ChatColor.UNDERLINE + "クリックで切替"
- );
- } else {
- lore = Arrays.asList(
- ChatColor.RESET + "" + ChatColor.RED + "現在音が鳴らない設定になっています。",
- ChatColor.RESET + "" + ChatColor.DARK_GRAY + "※この機能はデフォルトでONです。",
- ChatColor.RESET + "" + ChatColor.DARK_RED + "" + ChatColor.UNDERLINE + "クリックで切替"
- );
- itemmeta.addEnchant(Enchantment.DIG_SPEED, 100, false);
- }
- itemmeta.setLore(lore);
-
- return itemmeta;
- }
-
- /**
- * 投票妖精戦略
- * @param playerdata プレイヤーの設定
- * @return ラベルが付いたアイテム
- */
- private static ItemMeta getVotingFairyContractMeta(final PlayerData playerdata) {
- final ItemMeta itemmeta = Bukkit.getItemFactory().getItemMeta(Material.PAPER);
- itemmeta.setDisplayName(ChatColor.GOLD + "" + ChatColor.UNDERLINE + "" + ChatColor.BOLD + "妖精とのお約束");
- // n % 4 + 1 -> 1..4
- final int strategy = playerdata.toggleGiveApple();
- final List lore = loreTable.get(strategy);
-
- itemmeta.setLore(lore);
- return itemmeta;
- }
-
/**
* GiganticBerserk進化設定
* @param p
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/RankData.java b/src/main/scala/com/github/unchama/seichiassist/data/RankData.java
deleted file mode 100644
index 1e3dd5a65a..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/data/RankData.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.unchama.seichiassist.data;
-
-public class RankData {
- public String name;
- /* 整地Lv */
- public final int level;
- /* 整地量 */
- public final long totalbreaknum;
- /* プレイ時間 (ティック) */
- public long playtick;
- /* 投票回数 */
- public int p_vote;
- /* ??? */
- public int p_apple;
-
- public RankData() {
- name = "";
- level = 1;
- totalbreaknum = 0;
- }
-}
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/descrptions/PlayerStatsLoreGenerator.scala b/src/main/scala/com/github/unchama/seichiassist/data/descrptions/PlayerStatsLoreGenerator.scala
index b3838576dd..fcea2d0ff7 100644
--- a/src/main/scala/com/github/unchama/seichiassist/data/descrptions/PlayerStatsLoreGenerator.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/data/descrptions/PlayerStatsLoreGenerator.scala
@@ -13,6 +13,7 @@ import com.github.unchama.seichiassist.subsystems.ranking.domain.{
Ranking,
RankingRecordWithPosition
}
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
import com.github.unchama.seichiassist.util.TypeConverter
import org.bukkit.Bukkit
import org.bukkit.ChatColor._
@@ -25,7 +26,8 @@ class PlayerStatsLoreGenerator(
playerData: PlayerData,
seichiRanking: Ranking[SeichiAmountData],
seichiAmountData: SeichiAmountData,
- expBarVisibility: BreakCountBarVisibility
+ expBarVisibility: BreakCountBarVisibility,
+ voteAPI: VoteAPI[IO, Player]
) {
private val targetPlayer: Player = Bukkit.getPlayer(playerData.uuid)
@@ -167,11 +169,15 @@ class PlayerStatsLoreGenerator(
/**
* 連続投票日数の説明文.
*/
- private def totalChainVoteDaysDescription(): List[String] =
- if (playerData.ChainVote > 0)
- List(s"$RESET${GRAY}連続投票日数:${playerData.ChainVote}日")
+ private def totalChainVoteDaysDescription(): List[String] = {
+ val consecutiveVoteStreakDays =
+ voteAPI.currentConsecutiveVoteStreakDays(targetPlayer.getUniqueId).unsafeRunSync().value
+
+ if (consecutiveVoteStreakDays > 0)
+ List(s"$RESET${GRAY}連続投票日数:${consecutiveVoteStreakDays}日")
else
Nil
+ }
/**
* Expバーの説明文.
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
index 7cbaa14eb1..34b0cd9564 100644
--- a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
@@ -2,23 +2,22 @@ package com.github.unchama.seichiassist.data.player
import cats.effect.IO
import cats.effect.concurrent.Ref
-import com.github.unchama.generic.ClosedRange
import com.github.unchama.seichiassist._
import com.github.unchama.seichiassist.achievement.Nicknames
import com.github.unchama.seichiassist.data.GridTemplate
import com.github.unchama.seichiassist.data.player.settings.PlayerSettings
import com.github.unchama.seichiassist.subsystems.breakcount.domain.level.SeichiStarLevel
-import com.github.unchama.seichiassist.task.VotingFairyTask
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.domain.EffectPoint
+import com.github.unchama.seichiassist.util.RelativeDirection
import com.github.unchama.seichiassist.util.exp.{ExperienceManager, IExperienceManager}
-import com.github.unchama.seichiassist.util.{RelativeDirection, TimeUtils}
import com.github.unchama.targetedeffect.commandsender.MessageEffect
import org.bukkit.ChatColor._
import org.bukkit._
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
-import java.text.SimpleDateFormat
-import java.util.{GregorianCalendar, NoSuchElementException, UUID}
+import java.util.{NoSuchElementException, UUID}
import scala.collection.mutable
/**
@@ -44,7 +43,6 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
// プレイヤー名
val lowercaseName: String = name.toLowerCase()
- private val dummyDate = new GregorianCalendar(2100, 1, 1, 0, 0, 0)
// チェスト破壊トグル
var chestflag = true
@@ -81,27 +79,21 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
var playTick = 0L
// 合計経験値
var totalexp = 0L
- // 特典受け取り済み投票数
- var p_givenvote = 0
// 連続・通算ログイン用
// ロード時に初期化される
var lastcheckdate: String = _
var loginStatus: LoginStatus = LoginStatus(null)
// 期間限定ログイン用
var LimitedLoginCount = 0
- var ChainVote = 0
// region スキル関連のデータ
val skillState: Ref[IO, PlayerSkillState] = Ref.unsafe(PlayerSkillState.initial)
var skillEffectState: PlayerSkillEffectState = PlayerSkillEffectState.initial
- var effectPoint: Int = 0
// endregion
// 二つ名解禁フラグ保存用
var TitleFlags: mutable.BitSet = new mutable.BitSet(10001)
- // 二つ名関連用にp_vote(投票数)を引っ張る。(予期せぬエラー回避のため名前を複雑化)
- var p_vote_forT = 0
// 二つ名配布予約NOの保存
var giveachvNo = 0
// 実績ポイント用
@@ -110,15 +102,6 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
// n周年記念
var anniversary = false
var templateMap: mutable.Map[Int, GridTemplate] = mutable.HashMap()
- // 投票妖精関連
- var usingVotingFairy = false
- var voteFairyPeriod = new ClosedRange(dummyDate, dummyDate)
- var hasVotingFairyMana = 0
- var VotingFairyRecoveryValue = 0
- var toggleGiveApple = 1
- var toggleVotingFairy = 1
- var p_apple: Long = 0
- var toggleVFSound = true
var giganticBerserk: GiganticBerserk = GiganticBerserk()
// ハーフブロック破壊抑制用
@@ -164,7 +147,6 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
synchronizeDisplayNameToLevelState()
loadTotalExp()
- isVotingFairy()
}
// レベルを更新
@@ -247,27 +229,6 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
expmanager.setExp(totalexp)
}
- private def isVotingFairy(): Unit = {
- // 効果は継続しているか
- if (
- this.usingVotingFairy && !TimeUtils.isVotingFairyPeriod(
- this.votingFairyStartTime,
- this.votingFairyEndTime
- )
- ) {
- this.usingVotingFairy = false
- player.sendMessage(s"$LIGHT_PURPLE${BOLD}妖精は何処かへ行ってしまったようだ...")
- } else if (this.usingVotingFairy) {
- VotingFairyTask.speak(player, "おかえり!" + player.getName, true)
- }
- }
-
- def votingFairyEndTime: GregorianCalendar = voteFairyPeriod.endInclusive
-
- def votingFairyEndTime_=(value: GregorianCalendar): Unit = {
- voteFairyPeriod = new ClosedRange(voteFairyPeriod.start, value)
- }
-
def updateNickname(
id1: Int = settings.nickname.id1,
id2: Int = settings.nickname.id2,
@@ -320,27 +281,9 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
achievePoint = achievePoint.copy(used = achievePoint.used + amount)
}
- def convertEffectPointToAchievePoint(): Unit = {
+ def convertEffectPointToAchievePoint(implicit voteAPI: VoteAPI[IO, Player]): Unit = {
achievePoint = achievePoint.copy(conversionCount = achievePoint.conversionCount + 1)
- effectPoint -= 10
- }
-
- def calcPlayerApple(): Int = {
- // ランク用関数
- var i = 0
- val t = p_apple
-
- if (SeichiAssist.ranklist_p_apple.isEmpty) return 1
-
- var rankdata = SeichiAssist.ranklist_p_apple(i)
-
- // ランクが上がらなくなるまで処理
- while (rankdata.p_apple > t) {
- i += 1
- rankdata = SeichiAssist.ranklist_p_apple(i)
- }
-
- i + 1
+ voteAPI.decreaseEffectPoint(uuid, EffectPoint(10)).unsafeRunAsyncAndForget()
}
// パッシブスキルの獲得量表示
@@ -422,8 +365,6 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
}
}
- import com.github.unchama.seichiassist.AntiTypesafe
-
def addUnitAmount(direction: RelativeDirection, amount: Int): Unit = {
direction match {
case RelativeDirection.AHEAD =>
@@ -445,59 +386,6 @@ class PlayerData(@Deprecated() val uuid: UUID, val name: String) {
}
}
- @AntiTypesafe
- def getVotingFairyStartTimeAsString: String = {
- val cal = this.votingFairyStartTime
-
- if (votingFairyStartTime == dummyDate) {
- // 設定されてない場合
- ",,,,,"
- } else {
- // 設定されてる場合
- val date = cal.getTime
- val format = new SimpleDateFormat("yyyy,MM,dd,HH,mm,")
- format.format(date)
- }
- }
-
- def votingFairyStartTime: GregorianCalendar = voteFairyPeriod.start
-
- def votingFairyStartTime_=(value: GregorianCalendar): Unit = {
- voteFairyPeriod = new ClosedRange(value, voteFairyPeriod.endInclusive)
- }
-
- def setVotingFairyTime(@AntiTypesafe str: String): Unit = {
- val s = str.split(",")
- if (s.size < 5) return
- if (!s.slice(0, 5).contains("")) {
- val year = s(0).toInt
- val month = s(1).toInt - 1
- val dayOfMonth = s(2).toInt
- val starts = new GregorianCalendar(
- year,
- month,
- dayOfMonth,
- Integer.parseInt(s(3)),
- Integer.parseInt(s(4))
- )
-
- var min = Integer.parseInt(s(4)) + 1
- var hour = Integer.parseInt(s(3))
-
- min = if (this.toggleVotingFairy % 2 != 0) min + 30 else min
- hour = this.toggleVotingFairy match {
- case 2 | 3 => hour + 1
- case 4 => hour + 2
- case _ => hour
- }
-
- val ends = new GregorianCalendar(year, month, dayOfMonth, hour, min)
-
- this.votingFairyStartTime = starts
- this.votingFairyEndTime = ends
- }
- }
-
/**
* 運営権限により強制的に実績を解除することを試みる。
* 解除に成功し、このインスタンスが指す[Player]がオンラインであるならばその[Player]に解除の旨がチャットにて通知される。
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/player/VoteStatus.scala b/src/main/scala/com/github/unchama/seichiassist/data/player/VoteStatus.scala
deleted file mode 100644
index cfef5de615..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/data/player/VoteStatus.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.github.unchama.seichiassist.data.player
-
-import java.time.LocalDateTime
-
-case class VoteStatus(lastVote: LocalDateTime, chainLength: Int = 0, totalVote: Int = 0)
diff --git a/src/main/scala/com/github/unchama/seichiassist/database/manipulators/PlayerDataManipulator.scala b/src/main/scala/com/github/unchama/seichiassist/database/manipulators/PlayerDataManipulator.scala
index bce4e4a1cd..aab4dfd016 100644
--- a/src/main/scala/com/github/unchama/seichiassist/database/manipulators/PlayerDataManipulator.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/database/manipulators/PlayerDataManipulator.scala
@@ -1,129 +1,24 @@
package com.github.unchama.seichiassist.database.manipulators
import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.seichiassist.data.RankData
import com.github.unchama.seichiassist.data.player.PlayerData
import com.github.unchama.seichiassist.database.{DatabaseConstants, DatabaseGateway}
-import com.github.unchama.seichiassist.task.{CoolDownTask, PlayerDataLoading}
+import com.github.unchama.seichiassist.task.PlayerDataLoading
import com.github.unchama.util.ActionStatus
import org.bukkit.Bukkit
import org.bukkit.ChatColor._
-import org.bukkit.entity.Player
-import scalikejdbc.{DB, scalikejdbcSQLInterpolationImplicitDef}
import java.sql.SQLException
-import java.text.SimpleDateFormat
-import java.util.{Calendar, UUID}
+import java.util.UUID
import scala.collection.mutable
class PlayerDataManipulator(private val gateway: DatabaseGateway) {
import com.github.unchama.util.syntax.ResultSetSyntax._
- private val plugin = SeichiAssist.instance
-
private val tableReference: String =
s"${gateway.databaseName}.${DatabaseConstants.PLAYERDATA_TABLENAME}"
- // 投票特典配布時の処理(p_givenvoteの値の更新もココ)
- def compareVotePoint(player: Player, playerdata: PlayerData): Int = {
- ifCoolDownDoneThenGet(player, playerdata) {
- val struuid = playerdata.uuid.toString
-
- var p_vote = 0
- var p_givenvote = 0
-
- var command = s"select p_vote,p_givenvote from $tableReference where uuid = '$struuid'"
- try {
- gateway.executeQuery(command).recordIteration { lrs =>
- p_vote = lrs.getInt("p_vote")
- p_givenvote = lrs.getInt("p_givenvote")
- }
- } catch {
- case e: SQLException =>
- println("sqlクエリの実行に失敗しました。以下にエラーを表示します")
- e.printStackTrace()
- player.sendMessage(RED.toString + "投票特典の受け取りに失敗しました")
- return 0
- }
-
- // 比較して差があればその差の値を返す(同時にp_givenvoteも更新しておく)
- if (p_vote > p_givenvote) {
- command = ("update " + tableReference
- + " set p_givenvote = " + p_vote
- + s" where uuid = '$struuid'")
- if (gateway.executeUpdate(command) == ActionStatus.Fail) {
- player.sendMessage(RED.toString + "投票特典の受け取りに失敗しました")
- return 0
- }
-
- return p_vote - p_givenvote
- }
- player.sendMessage(YELLOW.toString + "投票特典は全て受け取り済みのようです")
- 0
- }
- }
-
- @inline private def ifCoolDownDoneThenGet(player: Player, playerdata: PlayerData)(
- supplier: => Int
- ): Int = {
- // 連打による負荷防止の為クールダウン処理
- if (!playerdata.votecooldownflag) {
- player.sendMessage(RED.toString + "しばらく待ってからやり直してください")
- return 0
- }
- new CoolDownTask(player, true, false).runTaskLater(plugin, 1200)
-
- supplier
- }
-
- /**
- * 投票ポイントをインクリメントするメソッド。
- *
- * @param playerName
- * プレーヤー名
- */
- def incrementVotePoint(playerName: String): Unit = {
- DB.localTx { implicit session =>
- sql"update playerdata set p_vote = p_vote + 1 where name = $playerName".update().apply()
- }
- }
-
- def addChainVote(name: String): Unit =
- DB.localTx { implicit session =>
- val calendar = Calendar.getInstance()
- val dateFormat = new SimpleDateFormat("yyyy/MM/dd")
-
- val lastVote =
- sql"SELECT lastvote FROM playerdata WHERE name = $name"
- .map(_.string("lastvote"))
- .single()
- .apply()
- .getOrElse(dateFormat.format(calendar.getTime))
-
- sql"UPDATE playerdata SET lastvote = ${dateFormat.format(calendar.getTime)} WHERE name = $name"
- .update()
- .apply()
-
- val TodayDate = dateFormat.parse(dateFormat.format(calendar.getTime))
- val LastDate = dateFormat.parse(lastVote)
- val TodayLong = TodayDate.getTime
- val LastLong = LastDate.getTime
-
- val dateDiff = (TodayLong - LastLong) / (1000 * 60 * 60 * 24)
- val shouldIncrementChainVote = dateDiff <= 2L
-
- val newCount = if (shouldIncrementChainVote) {
- sql"""select chainvote from playerdata where name = $name"""
- .map(_.int("chainvote"))
- .first()
- .apply()
- .get + 1
- } else 1
-
- sql"""update playerdata set chainvote = $newCount where name = $name"""
- }
-
// anniversary変更
def setAnniversary(anniversary: Boolean, uuid: Option[UUID]): Boolean = {
val command = s"UPDATE $tableReference SET anniversary = $anniversary" +
@@ -161,91 +56,6 @@ class PlayerDataManipulator(private val gateway: DatabaseGateway) {
uuidList.toList
}
- /**
- * 全ランキングリストの更新処理
- *
- * @return
- * 成否…true: 成功、false: 失敗 TODO この処理はDB上と通信を行う為非同期にすべき
- */
- def successRankingUpdate(): Boolean = {
- if (!successPlayTickRankingUpdate()) return false
- if (!successVoteRankingUpdate()) return false
- successAppleNumberRankingUpdate()
- }
-
- // ランキング表示用にプレイ時間のカラムだけ全員分引っ張る
- private def successPlayTickRankingUpdate(): Boolean = {
- val ranklist = mutable.ArrayBuffer[RankData]()
- val command = ("select name,playtick from " + tableReference
- + " order by playtick desc")
- try {
- gateway.executeQuery(command).recordIteration { lrs =>
- val rankdata = new RankData()
- rankdata.name = lrs.getString("name")
- rankdata.playtick = lrs.getLong("playtick")
- ranklist += rankdata
- }
- } catch {
- case e: SQLException =>
- println("sqlクエリの実行に失敗しました。以下にエラーを表示します")
- e.printStackTrace()
- return false
- }
-
- SeichiAssist.ranklist_playtick.clear()
- SeichiAssist.ranklist_playtick.addAll(ranklist)
- true
- }
-
- // ランキング表示用に投票数のカラムだけ全員分引っ張る
- private def successVoteRankingUpdate(): Boolean = {
- val ranklist = mutable.ArrayBuffer[RankData]()
- val command = ("select name,p_vote from " + tableReference
- + " order by p_vote desc")
- try {
- gateway.executeQuery(command).recordIteration { lrs =>
- val rankdata = new RankData()
- rankdata.name = lrs.getString("name")
- rankdata.p_vote = lrs.getInt("p_vote")
- ranklist += rankdata
- }
- } catch {
- case e: SQLException =>
- println("sqlクエリの実行に失敗しました。以下にエラーを表示します")
- e.printStackTrace()
- return false
- }
-
- SeichiAssist.ranklist_p_vote.clear()
- SeichiAssist.ranklist_p_vote.addAll(ranklist)
- true
- }
-
- // ランキング表示用に上げたりんご数のカラムだけ全員分引っ張る
- private def successAppleNumberRankingUpdate(): Boolean = {
- val ranklist = mutable.ArrayBuffer[RankData]()
- SeichiAssist.allplayergiveapplelong = 0
- val command = s"select name,p_apple from $tableReference order by p_apple desc"
- try {
- gateway.executeQuery(command).recordIteration { lrs =>
- val rankdata = new RankData()
- rankdata.name = lrs.getString("name")
- rankdata.p_apple = lrs.getInt("p_apple")
- ranklist += rankdata
- SeichiAssist.allplayergiveapplelong += rankdata.p_apple.toLong
- }
- } catch {
- case e: SQLException =>
- println("sqlクエリの実行に失敗しました。以下にエラーを表示します")
- e.printStackTrace()
- return false
- }
-
- SeichiAssist.ranklist_p_apple.clear()
- SeichiAssist.ranklist_p_apple.addAll(ranklist)
- true
- }
-
def loadPlayerData(playerUUID: UUID, playerName: String): PlayerData = {
val databaseGateway = SeichiAssist.databaseGateway
val table = DatabaseConstants.PLAYERDATA_TABLENAME
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/infrastructure/JdbcLastSeenNameToUuid.scala b/src/main/scala/com/github/unchama/seichiassist/infrastructure/minecraft/JdbcLastSeenNameToUuid.scala
similarity index 93%
rename from src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/infrastructure/JdbcLastSeenNameToUuid.scala
rename to src/main/scala/com/github/unchama/seichiassist/infrastructure/minecraft/JdbcLastSeenNameToUuid.scala
index 8340792ef4..446ef4d58b 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/infrastructure/JdbcLastSeenNameToUuid.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/infrastructure/minecraft/JdbcLastSeenNameToUuid.scala
@@ -1,4 +1,4 @@
-package com.github.unchama.seichiassist.subsystems.lastquit.infrastructure
+package com.github.unchama.seichiassist.infrastructure.minecraft
import cats.effect.Sync
import com.github.unchama.seichiassist.domain.actions.LastSeenNameToUuid
diff --git a/src/main/scala/com/github/unchama/seichiassist/infrastructure/scalikejdbc/ScalikeJDBCConfiguration.scala b/src/main/scala/com/github/unchama/seichiassist/infrastructure/scalikejdbc/ScalikeJDBCConfiguration.scala
index ef4f86e28c..28acc5ae32 100644
--- a/src/main/scala/com/github/unchama/seichiassist/infrastructure/scalikejdbc/ScalikeJDBCConfiguration.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/infrastructure/scalikejdbc/ScalikeJDBCConfiguration.scala
@@ -5,7 +5,13 @@ import scalikejdbc._
object ScalikeJDBCConfiguration {
private val connectionPoolSettings: ConnectionPoolSettings =
- ConnectionPoolSettings(initialSize = 5, maxSize = 20, connectionTimeoutMillis = 100000L)
+ ConnectionPoolSettings(
+ initialSize = 5,
+ maxSize = 20,
+ connectionTimeoutMillis = 100000L,
+ driverName =
+ "com.github.unchama.seichiassist.relocateddependencies.org.mariadb.jdbc.Driver"
+ )
private val loggingSettings: LoggingSQLAndTimeSettings = LoggingSQLAndTimeSettings(
enabled = true,
diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala
index 93e6d174d3..d320a98574 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala
@@ -1,36 +1,28 @@
package com.github.unchama.seichiassist.listener
-import cats.effect.{IO, SyncIO}
+import cats.effect.IO
import com.github.unchama.generic.effect.unsafe.EffectEnvironment
import com.github.unchama.menuinventory.router.CanOpen
import com.github.unchama.minecraft.actions.OnMinecraftServerThread
import com.github.unchama.seichiassist._
+import com.github.unchama.seichiassist.data.MenuInventoryData
import com.github.unchama.seichiassist.data.player.GiganticBerserk
-import com.github.unchama.seichiassist.data.{ItemData, MenuInventoryData}
-import com.github.unchama.seichiassist.effects.player.CommonSoundEffects
import com.github.unchama.seichiassist.listener.invlistener.OnClickTitleMenu
-import com.github.unchama.seichiassist.menus.achievement.AchievementMenu
-import com.github.unchama.seichiassist.menus.stickmenu.{FirstPage, StickMenu}
-import com.github.unchama.seichiassist.subsystems.gachaprize.bukkit.factories.BukkitGachaSkullData
-import com.github.unchama.seichiassist.subsystems.mana.ManaApi
-import com.github.unchama.seichiassist.task.VotingFairyTask
-import com.github.unchama.seichiassist.util.{InventoryOperations, TimeUtils}
+import com.github.unchama.seichiassist.util.InventoryOperations
+import com.github.unchama.seichiassist.menus.nicknames.NickNameMenu
import com.github.unchama.targetedeffect.commandsender.MessageEffect
import com.github.unchama.targetedeffect.player.FocusedSoundEffect
import org.bukkit.ChatColor._
import org.bukkit.enchantments.Enchantment
import org.bukkit.entity.{EntityType, Player}
-import org.bukkit.event.inventory.{InventoryClickEvent, InventoryCloseEvent, InventoryType}
+import org.bukkit.event.inventory.{InventoryClickEvent, InventoryCloseEvent}
import org.bukkit.event.{EventHandler, Listener}
-import org.bukkit.inventory.meta.SkullMeta
import org.bukkit.inventory.{ItemFlag, ItemStack}
import org.bukkit.{Bukkit, Material, Sound}
class PlayerInventoryListener(
implicit effectEnvironment: EffectEnvironment,
- manaApi: ManaApi[IO, SyncIO, Player],
- ioCanOpenFirstPage: IO CanOpen FirstPage.type,
- ioCanOpenAchievementMenu: IO CanOpen AchievementMenu.type,
+ ioCanOpenNicknameMenu: IO CanOpen NickNameMenu.type,
ioOnMainThread: OnMinecraftServerThread[IO]
) extends Listener {
@@ -39,7 +31,6 @@ class PlayerInventoryListener(
import com.github.unchama.util.syntax._
private val playerMap = SeichiAssist.playermap
- private val databaseGateway = SeichiAssist.databaseGateway
// 実績メニューの処理
@EventHandler
@@ -167,200 +158,6 @@ class PlayerInventoryListener(
)
}
- // 投票ptメニュー
- @EventHandler
- def onVotingMenuEvent(event: InventoryClickEvent): Unit = {
- // 外枠のクリック処理なら終了
- if (event.getClickedInventory == null) {
- return
- }
-
- val itemstackcurrent = event.getCurrentItem
- val view = event.getView
- val he = view.getPlayer
- // インベントリを開けたのがプレイヤーではない時終了
- if (he.getType != EntityType.PLAYER) {
- return
- }
-
- val topinventory = view.getTopInventory.ifNull {
- return
- }
- // インベントリが存在しない時終了
- // インベントリサイズが4列でない時終了
- if (topinventory.row != 4) {
- return
- }
- val player = he.asInstanceOf[Player]
- val uuid = player.getUniqueId
- val playerdata = playerMap(uuid)
-
- val playerLevel = SeichiAssist
- .instance
- .breakCountSystem
- .api
- .seichiAmountDataRepository(player)
- .read
- .unsafeRunSync()
- .levelCorrespondingToExp
- .level
-
- // インベントリ名が以下の時処理
- if (topinventory.getTitle == DARK_PURPLE.toString + "" + BOLD + "投票ptメニュー") {
- event.setCancelled(true)
-
- if (event.getClickedInventory.getType == InventoryType.PLAYER) {
- return
- }
-
- val isSkull = itemstackcurrent.getType == Material.SKULL_ITEM
-
- /*
- * クリックしたボタンに応じた各処理内容の記述ここから
- */
-
- // 投票pt受取
- if (itemstackcurrent.getType == Material.DIAMOND) {
- // nは特典をまだ受け取ってない投票分
- var n = databaseGateway.playerDataManipulator.compareVotePoint(player, playerdata)
- // 投票数に変化が無ければ処理終了
- if (n == 0) {
- return
- }
- // 先にp_voteの値を更新しておく
- playerdata.p_givenvote = playerdata.p_givenvote + n
-
- var count = 0
- while (n > 0) {
- // ここに投票1回につきプレゼントする特典の処理を書く
-
- // ガチャ券プレゼント処理
- val skull = BukkitGachaSkullData.gachaForVoting
- for { _ <- 0 to 9 } {
- if (
- player.getInventory.contains(skull) || !InventoryOperations.isPlayerInventoryFull(
- player
- )
- ) {
- InventoryOperations.addItem(player, skull)
- } else {
- InventoryOperations.dropItem(player, skull)
- }
- }
-
- // ピッケルプレゼント処理(レベル50になるまで)
- if (playerLevel < 50) {
- val pickaxe = ItemData.getSuperPickaxe(1)
- if (InventoryOperations.isPlayerInventoryFull(player)) {
- InventoryOperations.dropItem(player, pickaxe)
- } else {
- InventoryOperations.addItem(player, pickaxe)
- }
- }
-
- // 投票ギフト処理(レベル50から)
- if (playerLevel >= 50) {
- val gift = ItemData.getVotingGift(1)
- if (InventoryOperations.isPlayerInventoryFull(player)) {
- InventoryOperations.dropItem(player, gift)
- } else {
- InventoryOperations.addItem(player, gift)
- }
- }
- // エフェクトポイント加算処理
- playerdata.effectPoint += 10
-
- n -= 1
- count += 1
- }
-
- player.sendMessage(GOLD.toString + "投票特典" + WHITE + "(" + count + "票分)を受け取りました")
- player.playSound(player.getLocation, Sound.BLOCK_ANVIL_PLACE, 1f, 1f)
-
- val itemmeta = itemstackcurrent.getItemMeta
- itemstackcurrent.setItemMeta(itemmeta)
- player.openInventory(MenuInventoryData.getVotingMenuData(player))
- } else if (itemstackcurrent.getType == Material.BOOK_AND_QUILL) {
- // 投票リンク表示
- player.sendMessage(
- RED.toString + "" + UNDERLINE + "https://minecraft.jp/servers/54d3529e4ddda180780041a7/vote"
- )
- player.sendMessage(
- RED.toString + "" + UNDERLINE + "https://monocraft.net/servers/Cf3BffNIRMERDNbAfWQm"
- )
- player.playSound(player.getLocation, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f)
- player.closeInventory()
- } else if (
- isSkull && itemstackcurrent
- .getItemMeta
- .asInstanceOf[SkullMeta]
- .getOwningPlayer
- .getName == "MHF_ArrowLeft"
- ) {
-
- effectEnvironment.unsafeRunAsyncTargetedEffect(player)(
- SequentialEffect(
- CommonSoundEffects.menuTransitionFenceSound,
- ioCanOpenFirstPage.open(StickMenu.firstPage)
- ),
- "棒メニューの1ページ目を開く"
- )
-
- // NOTE: WHEN
- } else if (itemstackcurrent.getType == Material.WATCH) {
- player.playSound(player.getLocation, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f)
- playerdata.toggleVotingFairy = playerdata.toggleVotingFairy % 4 + 1
- player.openInventory(MenuInventoryData.getVotingMenuData(player))
- } else if (itemstackcurrent.getType == Material.PAPER) {
- player.playSound(player.getLocation, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f)
- playerdata.toggleGiveApple = playerdata.toggleGiveApple % 4 + 1
- player.openInventory(MenuInventoryData.getVotingMenuData(player))
- } else if (itemstackcurrent.getType == Material.JUKEBOX) {
- player.playSound(player.getLocation, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f)
- playerdata.toggleVFSound = !playerdata.toggleVFSound
- player.openInventory(MenuInventoryData.getVotingMenuData(player))
- } else if (itemstackcurrent.getType == Material.GHAST_TEAR) {
- player.closeInventory()
-
- // プレイヤーレベルが10に達していないとき
- if (playerLevel < 10) {
- player.sendMessage(GOLD.toString + "プレイヤーレベルが足りません")
- player.playSound(player.getLocation, Sound.BLOCK_GLASS_PLACE, 1f, 0.1f)
- return
- }
-
- // 既に妖精召喚している場合終了
- if (playerdata.usingVotingFairy) {
- player.sendMessage(GOLD.toString + "既に妖精を召喚しています")
- player.playSound(player.getLocation, Sound.BLOCK_GLASS_PLACE, 1f, 0.1f)
- return
- }
-
- // 投票ptが足りない場合終了
- if (playerdata.effectPoint < playerdata.toggleVotingFairy * 2) {
- player.sendMessage(GOLD.toString + "投票ptが足りません")
- player.playSound(player.getLocation, Sound.BLOCK_GLASS_PLACE, 1f, 0.1f)
- return
- }
-
- VotingFairyListener.summon(player)
- player.closeInventory()
- } else if (itemstackcurrent.getType == Material.COMPASS) {
- VotingFairyTask.speak(
- player,
- "僕は" + TimeUtils.showHour(playerdata.votingFairyEndTime) + "には帰るよー。",
- playerdata.toggleVFSound
- )
- player.closeInventory()
- } // 妖精召喚
- // 妖精音トグル
- // 妖精リンゴトグル
- // 妖精時間トグル
- // 棒メニューに戻る
-
- }
- }
-
@EventHandler
def onGiganticBerserkMenuEvent(event: InventoryClickEvent): Unit = {
// 外枠のクリック処理なら終了
diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerJoinListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerJoinListener.scala
index f585e8b4dd..765ac5277f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerJoinListener.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerJoinListener.scala
@@ -104,16 +104,18 @@ class PlayerJoinListener extends Listener {
)
SendSoundEffect.sendEverySound(Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f)
- // ルール熟読をタイトルとチャットで迫る
- // サブタイトルと分ける理由はGUIサイズによって見切れる可能性があるため
+ // 同時に【はじめての方へ】ページに誘導したほうがただWebサイトに誘導するよりまだ可能性がありそう
+ // https://github.com/GiganticMinecraft/SeichiAssist/issues/1939
player.sendTitle(
- s"${YELLOW}ルールは確認されましたか?",
- s"${LIGHT_PURPLE}公式サイトで確認してください",
+ s"${YELLOW}ようこそ! ギガンティック☆整地鯖へ!",
+ s"${LIGHT_PURPLE}まず初めに公式サイト【はじめての方へ】ページを確認してください",
10,
20 * 10, // タイトルの表示時間は10秒
10
)
- player.sendMessage(s"${YELLOW}ルール→ $YELLOW${UNDERLINE}https://www.seichi.network/rule")
+ player.sendMessage(
+ s"$YELLOW【はじめての方へ】ページ→ $YELLOW${UNDERLINE}https://www.seichi.network/helloworld"
+ )
import scala.util.chaining._
diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/VotingFairyListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/VotingFairyListener.scala
deleted file mode 100644
index 88311dd823..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/listener/VotingFairyListener.scala
+++ /dev/null
@@ -1,229 +0,0 @@
-package com.github.unchama.seichiassist.listener
-
-import cats.effect.{IO, SyncIO}
-import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.seichiassist.subsystems.breakcount.domain.level.SeichiLevel
-import com.github.unchama.seichiassist.subsystems.mana.domain.ManaAmount
-import com.github.unchama.seichiassist.subsystems.mana.{ManaApi, ManaReadApi}
-import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
-import com.github.unchama.seichiassist.task.VotingFairyTask
-import com.github.unchama.seichiassist.util.TimeUtils
-import org.bukkit.ChatColor._
-import org.bukkit.entity.Player
-import org.bukkit.event.Listener
-import org.bukkit.inventory.ItemStack
-
-import java.util.{Calendar, GregorianCalendar}
-import scala.util.Random
-
-object VotingFairyListener {
- def summon(p: Player)(implicit manaApi: ManaReadApi[IO, SyncIO, Player]): Unit = {
- val playermap = SeichiAssist.playermap
- val uuid = p.getUniqueId
- val playerdata = playermap.apply(uuid)
-
- // 召喚した時間を取り出す
- playerdata.votingFairyStartTime = new GregorianCalendar(
- Calendar.getInstance.get(Calendar.YEAR),
- Calendar.getInstance.get(Calendar.MONTH),
- Calendar.getInstance.get(Calendar.DATE),
- Calendar.getInstance.get(Calendar.HOUR_OF_DAY),
- Calendar.getInstance.get(Calendar.MINUTE)
- )
-
- var min = Calendar.getInstance.get(Calendar.MINUTE) + 1
- var hour = Calendar.getInstance.get(Calendar.HOUR_OF_DAY)
- min = if ((playerdata.toggleVotingFairy % 2) != 0) min + 30 else min
- hour =
- if (playerdata.toggleVotingFairy == 2) hour + 1
- else if (playerdata.toggleVotingFairy == 3) hour + 1
- else if (playerdata.toggleVotingFairy == 4) hour + 2
- else hour
-
- playerdata.votingFairyEndTime = new GregorianCalendar(
- Calendar.getInstance.get(Calendar.YEAR),
- Calendar.getInstance.get(Calendar.MONTH),
- Calendar.getInstance.get(Calendar.DATE),
- hour,
- min
- )
-
- // 投票ptを減らす
- playerdata.effectPoint_$eq(playerdata.effectPoint - playerdata.toggleVotingFairy * 2)
-
- // フラグ
- playerdata.usingVotingFairy = true
-
- // マナ回復量最大値の決定
- val n = manaApi.readManaAmount(p).unsafeRunSync().cap.value
- playerdata.VotingFairyRecoveryValue =
- ((n / 10 - n / 30 + new Random().nextInt((n / 20).toInt)) / 2.9).toInt + 200
-
- p.sendMessage(s"$RESET$YELLOW${BOLD}妖精を呼び出しました!")
- p.sendMessage(s"$RESET$YELLOW${BOLD}この子は1分間に約${playerdata.VotingFairyRecoveryValue}マナ")
- p.sendMessage(s"$RESET$YELLOW${BOLD}回復させる力を持っているようです。")
-
- // メッセージ
-
- val morning = List(
- "おはよ![str1]",
- "ヤッホー[str1]!",
- "ふわぁ。。。[str1]の朝は早いね。",
- "うーん、今日も一日頑張ろ!",
- "今日は整地日和だね![str1]!"
- )
- val day = List(
- "やあ![str1]",
- "ヤッホー[str1]!",
- "あっ、[str1]じゃん。丁度お腹空いてたんだ!",
- "この匂い…[str1]ってがちゃりんごいっぱい持ってる…?",
- "今日のおやつはがちゃりんごいっぱいだ!"
- )
- val night = List(
- "やあ![str1]",
- "ヤッホー[str1]!",
- "ふわぁ。。。[str1]は夜も元気だね。",
- "もう寝ようと思ってたのにー。[str1]はしょうがないなぁ",
- "こんな時間に呼ぶなんて…りんごははずんでもらうよ?"
- )
-
- if (TimeUtils.getTimeZone(playerdata.votingFairyStartTime) == "morning") {
- VotingFairyTask.speak(p, getMessage(morning, p.getName), playerdata.toggleVFSound)
- } else if (TimeUtils.getTimeZone(playerdata.votingFairyStartTime) == "day") {
- VotingFairyTask.speak(p, getMessage(day, p.getName), playerdata.toggleVFSound)
- } else VotingFairyTask.speak(p, getMessage(night, p.getName), playerdata.toggleVFSound)
- }
-
- private def getMessage(messages: List[String], str1: String) = {
- val msg = messages(Random.nextInt(messages.size))
- if (str1.nonEmpty) msg.replace("[str1]", str1 + RESET)
- else msg
- }
-
- def regeneMana(player: Player)(
- implicit manaApi: ManaApi[IO, SyncIO, Player],
- mineStackAPI: MineStackAPI[IO, Player, ItemStack]
- ): Unit = {
- val playermap = SeichiAssist.playermap
- val uuid = player.getUniqueId
- val playerdata = playermap.apply(uuid)
-
- val oldManaAmount = manaApi.readManaAmount(player).unsafeRunSync()
-
- if (oldManaAmount.isFull) {
- // マナが最大だった場合はメッセージを送信して終わり
- val msg = List(
- "整地しないのー?",
- "たくさん働いて、たくさんりんごを食べようね!",
- "僕はいつか大きながちゃりんごを食べ尽して見せるっ!",
- "ちょっと食べ疲れちゃった",
- "[str1]はどのりんごが好き?僕はがちゃりんご!",
- "動いてお腹を空かしていっぱい食べるぞー!"
- )
- VotingFairyTask.speak(player, getMessage(msg, player.getName), playerdata.toggleVFSound)
- } else {
- val playerLevel =
- SeichiAssist
- .instance
- .breakCountSystem
- .api
- .seichiAmountDataRepository(player)
- .read
- .unsafeRunSync()
- .levelCorrespondingToExp
-
- var n = playerdata.VotingFairyRecoveryValue // 実際のマナ回復量
- var m = getGiveAppleValue(playerLevel) // りんご消費量
-
- // 連続投票によってりんご消費量を抑える
- if (playerdata.ChainVote >= 30) m /= 2
- else if (playerdata.ChainVote >= 10) m = (m / 1.5).toInt
- else if (playerdata.ChainVote >= 3) m = (m / 1.25).toInt
-
- // トグルで数値変更
- if (playerdata.toggleGiveApple == 2)
- if (oldManaAmount.ratioToCap.exists(_ >= 0.75)) {
- n /= 2
- m /= 2
- } else if (playerdata.toggleGiveApple == 3) {
- n /= 2
- m /= 2
- }
-
- if (m == 0) m = 1
-
- if (playerdata.toggleGiveApple == 4) {
- n /= 4
- m = 0
- } else {
- // ちょっとつまみ食いする
- if (m >= 10) m += new Random().nextInt(m / 10)
- }
-
- // りんご所持数で値変更
- val gachaRingo =
- mineStackAPI.mineStackObjectList.findByName("gachaimo").unsafeRunSync().get
- val quantity =
- mineStackAPI.mineStackRepository.getStackedAmountOf(player, gachaRingo).unsafeRunSync()
- if (m > quantity) {
- if (quantity == 0) {
- n /= 2
- if (playerdata.toggleGiveApple == 1) n /= 2
- if (playerdata.toggleGiveApple == 2 && oldManaAmount.ratioToCap.exists(_ < 0.75))
- n /= 2
- player.sendMessage(s"$RESET$YELLOW${BOLD}MineStackにがちゃりんごがないようです。。。")
- } else {
- val M = m
- val L = quantity
- n = if ((L / M) <= 0.5) (n * 0.5).toInt else (n * L / M).toInt
- }
-
- m = quantity.toInt
- }
-
- // 回復量に若干乱数をつける
- n = (n - n / 100) + Random.nextInt(n / 50)
-
- // マナ回復
- manaApi.manaAmount(player).restoreAbsolute(ManaAmount(n)).unsafeRunSync()
-
- // りんごを減らす
- mineStackAPI
- .mineStackRepository
- .subtractStackedAmountOf(player, gachaRingo, m)
- .unsafeRunAsyncAndForget()
-
- // 減ったりんごの数をplayerdataに加算
- playerdata.p_apple += m
-
- val yes =
- List("(´~`)モグモグ…", "がちゃりんごって美味しいよね!", "あぁ!幸せ!", "[str1]のりんごはおいしいなぁ", "いつもりんごをありがとう!")
- val no = List(
- "お腹空いたなぁー。",
- "がちゃりんごがっ!食べたいっ!",
- "(´;ω;`)ウゥゥ ヒモジイ...",
- "@うんちゃま [str1]が意地悪するんだっ!",
- "うわーん!お腹空いたよー!"
- )
-
- player.sendMessage(s"$RESET$YELLOW${BOLD}マナ妖精が${n}マナを回復してくれました")
-
- if (m != 0) {
- player.sendMessage(s"$RESET$YELLOW${BOLD}あっ!${m}個のがちゃりんごが食べられてる!")
- VotingFairyTask.speak(player, getMessage(yes, player.getName), playerdata.toggleVFSound)
- } else {
- player.sendMessage(s"$RESET$YELLOW${BOLD}あなたは妖精にりんごを渡しませんでした。")
- VotingFairyTask.speak(player, getMessage(no, player.getName), playerdata.toggleVFSound)
- }
- }
- }
-
- private def getGiveAppleValue(playerLevel: SeichiLevel): Int = {
- // 10で切り捨て除算して二乗する。最低でも1は返す。
- val levelDividedByTen = playerLevel.level / 10
-
- (levelDividedByTen * levelDividedByTen) max 1
- }
-}
-
-class VotingFairyListener extends Listener {}
diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/invlistener/OnClickTitleMenu.scala b/src/main/scala/com/github/unchama/seichiassist/listener/invlistener/OnClickTitleMenu.scala
index d671dca91c..4122f4b310 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/invlistener/OnClickTitleMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/invlistener/OnClickTitleMenu.scala
@@ -1,15 +1,12 @@
package com.github.unchama.seichiassist.listener.invlistener
import cats.effect.IO
-import com.github.unchama.generic.effect.unsafe.EffectEnvironment
import com.github.unchama.menuinventory.router.CanOpen
import com.github.unchama.seichiassist.SeichiAssist
import com.github.unchama.seichiassist.achievement.Nicknames
import com.github.unchama.seichiassist.data.MenuInventoryData
import com.github.unchama.seichiassist.data.MenuInventoryData.MenuType
-import com.github.unchama.seichiassist.effects.player.CommonSoundEffects
-import com.github.unchama.seichiassist.menus.achievement.AchievementMenu
-import com.github.unchama.targetedeffect.SequentialEffect
+import com.github.unchama.seichiassist.menus.nicknames.NickNameMenu
import org.bukkit.entity.{EntityType, Player}
import org.bukkit.event.inventory.{InventoryClickEvent, InventoryType}
import org.bukkit.inventory.ItemStack
@@ -24,16 +21,12 @@ object OnClickTitleMenu {
private def clickedSound(player: Player, sound: Sound, pitch: Float): Unit =
player.playSound(player.getLocation, sound, 1f, pitch)
- private def isApplicableAsPrevPageButton(is: ItemStack): Boolean =
- is.getItemMeta.asInstanceOf[SkullMeta].getOwningPlayer.getName == "MHF_ArrowLeft"
-
private def isApplicableAsNextPageButton(is: ItemStack): Boolean =
is.getItemMeta.asInstanceOf[SkullMeta].getOwningPlayer.getName == "MHF_ArrowRight"
- def onPlayerClickTitleMenuEvent(event: InventoryClickEvent)(
- implicit effectEnvironment: EffectEnvironment,
- ioCanOpenAchievementMenu: IO CanOpen AchievementMenu.type
- ): Unit = {
+ def onPlayerClickTitleMenuEvent(
+ event: InventoryClickEvent
+ )(implicit ioCanOpenNicknameMenu: IO CanOpen NickNameMenu.type): Unit = {
import com.github.unchama.util.syntax.Nullability.NullabilityExtensionReceiver
// 外枠のクリック処理なら終了
@@ -77,64 +70,6 @@ object OnClickTitleMenu {
val mat = current.getType
val isSkull = mat == Material.SKULL_ITEM
topInventory.getTitle match {
- case MenuType.COMBINE.invName =>
- event.setCancelled(true)
- // 二つ名組み合わせトップ
- mat match {
- // 実績ポイント最新化
- case Material.EMERALD_ORE =>
- clickedSound(player, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f)
-
- // エフェクトポイント→実績ポイント変換
- case Material.EMERALD =>
- clickedSound(player, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f)
- if (pd.effectPoint >= 10) {
- pd.convertEffectPointToAchievePoint()
- } else {
- player.sendMessage("エフェクトポイントが不足しています。")
- }
-
- // パーツショップ
- case Material.ITEM_FRAME =>
- clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computePartsShopMenu(player))
-
- // 前パーツ
- case Material.WATER_BUCKET =>
- clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeHeadPartCustomMenu(player))
-
- // 中パーツ
- case Material.MILK_BUCKET =>
- clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeMiddlePartCustomMenu(player))
-
- // 後パーツ
- case Material.LAVA_BUCKET =>
- clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeTailPartCustomMenu(player))
-
- case _ if isSkull && isApplicableAsPrevPageButton(current) =>
- effectEnvironment.unsafeRunAsyncTargetedEffect(player)(
- SequentialEffect(
- CommonSoundEffects.menuTransitionFenceSound,
- ioCanOpenAchievementMenu.open(AchievementMenu)
- ),
- "実績メニューを開く"
- )
-
- case _ =>
- }
-
- // refresh if needed
- mat match {
- case Material.EMERALD_ORE | Material.EMERALD =>
- pd.recalculateAchievePoint()
- player.openInventory(MenuInventoryData.computeRefreshedCombineMenu(player))
-
- case _ =>
- }
-
case MenuType.HEAD.invName =>
event.setCancelled(true)
mat match {
@@ -165,7 +100,7 @@ object OnClickTitleMenu {
case Material.BARRIER =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeRefreshedCombineMenu(player))
+ ioCanOpenNicknameMenu.open(NickNameMenu).apply(player).unsafeRunAsyncAndForget()
case _ if isSkull && isApplicableAsNextPageButton(current) =>
// 次ページ
@@ -211,7 +146,7 @@ object OnClickTitleMenu {
case Material.BARRIER =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeRefreshedCombineMenu(player))
+ ioCanOpenNicknameMenu.open(NickNameMenu).apply(player).unsafeRunAsyncAndForget()
case _ if isSkull && isApplicableAsNextPageButton(current) =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
@@ -256,7 +191,7 @@ object OnClickTitleMenu {
case Material.BARRIER =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeRefreshedCombineMenu(player))
+ ioCanOpenNicknameMenu.open(NickNameMenu).apply(player).unsafeRunAsyncAndForget()
case _ if isSkull && isApplicableAsNextPageButton(current) =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
@@ -304,7 +239,7 @@ object OnClickTitleMenu {
case Material.BARRIER =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
- player.openInventory(MenuInventoryData.computeRefreshedCombineMenu(player))
+ ioCanOpenNicknameMenu.open(NickNameMenu).apply(player).unsafeRunAsyncAndForget()
case _ if isSkull && isApplicableAsNextPageButton(current) =>
clickedSound(player, Sound.BLOCK_FENCE_GATE_OPEN, 0.1f)
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
index baef15275d..ca4af784ec 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
@@ -16,6 +16,7 @@ import com.github.unchama.seichiassist.menus.minestack.{
MineStackMainMenu,
MineStackSelectItemColorMenu
}
+import com.github.unchama.seichiassist.menus.nicknames.NickNameMenu
import com.github.unchama.seichiassist.menus.ranking.{RankingMenu, RankingRootMenu}
import com.github.unchama.seichiassist.menus.skill.{
ActiveSkillEffectMenu,
@@ -47,6 +48,9 @@ import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
import com.github.unchama.seichiassist.subsystems.ranking.api.AssortedRankingApi
import com.github.unchama.seichiassist.subsystems.ranking.domain.values.{LoginTime, VoteCount}
import com.github.unchama.seichiassist.subsystems.sharedinventory.SharedInventoryAPI
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.FairyAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.FairySpeechAPI
import io.chrisdavenport.cats.effect.time.JavaTime
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
@@ -59,6 +63,8 @@ trait TopLevelRouter[F[_]] {
implicit val ioCanOpenCategorizedMineStackMenu: F CanOpen CategorizedMineStackMenu
+ implicit val ioCanOpenNickNameMenu: F CanOpen NickNameMenu.type
+
}
object TopLevelRouter {
@@ -80,12 +86,15 @@ object TopLevelRouter {
homeReadApi: HomeReadAPI[IO],
enderChestAccessApi: AnywhereEnderChestAPI[IO],
sharedInventoryAPI: SharedInventoryAPI[IO, Player],
+ voteAPI: VoteAPI[IO, Player],
+ fairyAPI: FairyAPI[IO, SyncIO, Player],
donateAPI: DonatePremiumPointAPI[IO],
gachaTicketAPI: GachaTicketAPI[IO],
gachaPrizeAPI: GachaPrizeAPI[IO, ItemStack, Player],
mineStackAPI: MineStackAPI[IO, Player, ItemStack],
gachaDrawAPI: GachaDrawAPI[IO, Player],
- consumeGachaTicketAPI: ConsumeGachaTicketAPI[IO, Player]
+ consumeGachaTicketAPI: ConsumeGachaTicketAPI[IO, Player],
+ fairySpeechAPI: FairySpeechAPI[IO, Player]
): TopLevelRouter[IO] = new TopLevelRouter[IO] {
import assortedRankingApi._
@@ -104,6 +113,8 @@ object TopLevelRouter {
new PremiumPointTransactionHistoryMenu.Environment
implicit lazy val serverSwitchMenuEnv: ServerSwitchMenu.Environment =
new ServerSwitchMenu.Environment
+ implicit lazy val nickNameMenuEnv: NickNameMenu.Environment =
+ new NickNameMenu.Environment
implicit lazy val achievementMenuEnv: AchievementMenu.Environment =
new AchievementMenu.Environment
implicit lazy val homeMenuEnv: HomeMenu.Environment = new HomeMenu.Environment
@@ -131,8 +142,14 @@ object TopLevelRouter {
implicit lazy val rankingRootMenuEnv: RankingRootMenu.Environment =
new RankingRootMenu.Environment
+ implicit lazy val voteMenuEnv: VoteMenu.Environment = new VoteMenu.Environment
+
implicit lazy val stickMenuEnv: FirstPage.Environment = new FirstPage.Environment
+ implicit lazy val ioCanOpenVoteMenu: IO CanOpen VoteMenu.type = _.open
+
+ implicit lazy val ioCanOpenNickNameMenu: IO CanOpen NickNameMenu.type = _.open
+
implicit lazy val ioCanOpenSelectItemColorMenu: IO CanOpen MineStackSelectItemColorMenu =
_.open
implicit lazy val ioCanOpenAchievementGroupMenu: IO CanOpen AchievementGroupMenu = _.open
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/VoteMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/VoteMenu.scala
new file mode 100644
index 0000000000..d4537c6b78
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/VoteMenu.scala
@@ -0,0 +1,417 @@
+package com.github.unchama.seichiassist.menus
+
+import cats.data.{Kleisli, NonEmptyList}
+import cats.effect.{ConcurrentEffect, IO, SyncIO}
+import cats.implicits._
+import com.github.unchama.itemstackbuilder.IconItemStackBuilder
+import com.github.unchama.menuinventory.router.CanOpen
+import com.github.unchama.menuinventory.slot.button.action.LeftClickButtonEffect
+import com.github.unchama.menuinventory.slot.button.{Button, RecomputedButton}
+import com.github.unchama.menuinventory.syntax.IntInventorySizeOps
+import com.github.unchama.menuinventory.{ChestSlotRef, Menu, MenuFrame, MenuSlotLayout}
+import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts
+import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.onMainThread
+import com.github.unchama.seichiassist.menus.stickmenu.FirstPage
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.FairyAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyAppleConsumeStrategy.{
+ Consume,
+ LessConsume,
+ NoConsume,
+ Permissible
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairySummonRequestError.{
+ AlreadyFairySummoned,
+ NotEnoughEffectPoint,
+ NotEnoughSeichiLevel
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.{
+ FairyAppleConsumeStrategy,
+ FairyLore,
+ FairySummonCost
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.FairySpeechAPI
+import com.github.unchama.targetedeffect.TargetedEffect.emptyEffect
+import com.github.unchama.targetedeffect.commandsender.MessageEffect
+import com.github.unchama.targetedeffect.player.FocusedSoundEffect
+import com.github.unchama.targetedeffect.player.PlayerEffects.closeInventoryEffect
+import com.github.unchama.targetedeffect.{DeferredEffect, SequentialEffect}
+import org.bukkit.ChatColor._
+import org.bukkit.entity.Player
+import org.bukkit.{Material, Sound}
+
+object VoteMenu extends Menu {
+
+ class Environment(
+ implicit val voteAPI: VoteAPI[IO, Player],
+ val fairyAPI: FairyAPI[IO, SyncIO, Player],
+ val ioCanOpenFirstPage: IO CanOpen FirstPage.type,
+ val fairySpeechAPI: FairySpeechAPI[IO, Player]
+ )
+
+ /**
+ * メニューのサイズとタイトルに関する情報
+ */
+ override val frame: MenuFrame = MenuFrame(4.chestRows, s"$DARK_PURPLE${BOLD}投票ptメニュー")
+
+ /**
+ * @return
+ * `player`からメニューの[[MenuSlotLayout]]を計算する[[IO]]
+ */
+ override def computeMenuLayout(
+ player: Player
+ )(implicit environment: Environment): IO[MenuSlotLayout] = {
+ import environment._
+ import eu.timepit.refined.auto._
+ val constantButtons = ConstantButtons(player)
+ import constantButtons._
+
+ val staticButtons =
+ Map(
+ ChestSlotRef(1, 0) -> showVoteURLButton,
+ ChestSlotRef(3, 0) -> CommonButtons.openStickMenu
+ )
+
+ val computeButtonsIO =
+ Seq(
+ ChestSlotRef(0, 0) -> receiveVoteBenefitsButton,
+ ChestSlotRef(0, 4) -> fairySummonButton,
+ ChestSlotRef(0, 2) -> fairySummonTimeToggleButton,
+ ChestSlotRef(1, 2) -> fairyContractSettingToggle,
+ ChestSlotRef(2, 2) -> fairyPlaySoundToggleButton
+ ).traverse(_.sequence)
+
+ val dynamicButtonsIO =
+ Seq(ChestSlotRef(0, 6) -> gachaRingoInformation, ChestSlotRef(1, 4) -> checkTimeButton)
+ .traverse(_.sequence)
+
+ for {
+ isFairyUsing <- environment.fairyAPI.isFairyAppearing(constantButtons.player)
+ computeButtons <- computeButtonsIO
+ dynamicButtons <- dynamicButtonsIO
+ } yield {
+ val exceptDynamicButtons = staticButtons ++ computeButtons
+ MenuSlotLayout(exceptDynamicButtons).merge(
+ if (isFairyUsing)
+ MenuSlotLayout(dynamicButtons: _*)
+ else MenuSlotLayout.emptyLayout
+ )
+ }
+ }
+
+ private case class ConstantButtons(player: Player)(
+ implicit voteAPI: VoteAPI[IO, Player],
+ fairyAPI: FairyAPI[IO, SyncIO, Player],
+ fairySpeechAPI: FairySpeechAPI[IO, Player]
+ ) {
+
+ import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.layoutPreparationContext
+
+ private val uuid = player.getUniqueId
+
+ private implicit val ioCE: ConcurrentEffect[IO] =
+ IO.ioConcurrentEffect(PluginExecutionContexts.asyncShift)
+
+ val receiveVoteBenefitsButton: IO[Button] = RecomputedButton {
+ val uuid = player.getUniqueId
+ for {
+ benefits <- voteAPI.receivedVoteBenefits(uuid)
+ voteCounter <- voteAPI.count(uuid)
+ effectPoint <- voteAPI.effectPoints(player)
+ notReceivedBenefits = voteCounter.value - benefits.value
+ } yield {
+ Button(
+ new IconItemStackBuilder(Material.DIAMOND)
+ .title(s"$LIGHT_PURPLE$UNDERLINE${BOLD}クリックで投票特典を受け取れます")
+ .lore(
+ List(
+ s"$RESET${GRAY}投票特典を受け取るには",
+ s"$RESET${GRAY}投票ページで投票した後",
+ s"$RESET${AQUA}特典受け取り済み投票回数: ${benefits.value}",
+ s"$RESET${AQUA}特典未受け取り投票係数: $notReceivedBenefits",
+ s"$RESET${AQUA}所有pt: ${effectPoint.value}"
+ )
+ )
+ .enchanted()
+ .build(),
+ LeftClickButtonEffect {
+ if (notReceivedBenefits == 0) {
+ MessageEffect(s"$YELLOW${BOLD}投票特典はすべて受け取り済みのようです")
+ } else {
+ SequentialEffect(
+ DeferredEffect(IO(voteAPI.receiveVoteBenefits)),
+ MessageEffect(s"${GOLD}投票特典$WHITE(${notReceivedBenefits}票分)を受け取りました")
+ )
+ }
+ }
+ )
+ }
+ }
+
+ val showVoteURLButton: Button = Button(
+ new IconItemStackBuilder(Material.BOOK_AND_QUILL)
+ .title(s"$YELLOW$UNDERLINE${BOLD}投票ページにアクセス")
+ .lore(
+ List(
+ s"$RESET${GREEN}投票すると様々な特典が!",
+ s"$RESET${GREEN}1日1回投票できます",
+ s"$RESET${DARK_GRAY}クリックするとチャット欄に",
+ s"$RESET${DARK_GRAY}URLが表示されますので",
+ s"$RESET${DARK_GRAY}Tキーを押してから",
+ s"$RESET${DARK_GRAY}そのURLをクリックしてください"
+ )
+ )
+ .build(),
+ LeftClickButtonEffect {
+ SequentialEffect(
+ MessageEffect(
+ List(
+ s"$RED${UNDERLINE}https://minecraft.jp/servers/54d3529e4ddda180780041a7/vote",
+ s"$RED${UNDERLINE}https://monocraft.net/servers/Cf3BffNIRMERDNbAfWQm"
+ )
+ ),
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f),
+ closeInventoryEffect
+ )
+ }
+ )
+
+ val fairySummonTimeToggleButton: IO[Button] = {
+ RecomputedButton(for {
+ fairySummonCost <- fairyAPI.fairySummonCost(player)
+ } yield {
+ Button(
+ new IconItemStackBuilder(Material.WATCH)
+ .title(s"$AQUA$UNDERLINE${BOLD}マナ妖精 時間設定")
+ .lore(
+ List(
+ s"$RESET$GREEN$BOLD${fairySummonCostToString(fairySummonCost)}",
+ "",
+ s"$RESET${GRAY}コスト",
+ s"$RESET$RED$BOLD${fairySummonCost.value * 2}投票pt",
+ "",
+ s"$RESET$DARK_RED${UNDERLINE}クリックで切り替え"
+ )
+ )
+ .build(),
+ LeftClickButtonEffect {
+ SequentialEffect(
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f),
+ DeferredEffect(
+ IO(
+ fairyAPI.updateFairySummonCost(FairySummonCost(fairySummonCost.value % 4 + 1))
+ )
+ )
+ )
+ }
+ )
+ })
+ }
+
+ val fairyContractSettingToggle: IO[Button] = {
+ val appleConsumeStrategyDependency
+ : Map[FairyAppleConsumeStrategy, FairyAppleConsumeStrategy] =
+ Map(
+ Permissible -> Consume,
+ Consume -> LessConsume,
+ LessConsume -> NoConsume,
+ NoConsume -> Permissible
+ )
+
+ val fairyLoreTable: Map[FairyAppleConsumeStrategy, FairyLore] = Map(
+ Permissible -> FairyLore(
+ NonEmptyList.of(
+ s"$RED$UNDERLINE${BOLD}ガンガンたべるぞ",
+ s"$RESET${GRAY}とにかく妖精さんにりんごを開放します。",
+ s"$RESET${GRAY}めっちゃ喜ばれます。"
+ )
+ ),
+ Consume -> FairyLore(
+ NonEmptyList.of(
+ s"$YELLOW$UNDERLINE${BOLD}バッチリたべよう",
+ s"$RESET${GRAY}食べ過ぎないように注意しつつ",
+ s"$RESET${GRAY}妖精さんにりんごを開放します。",
+ s"$RESET${GRAY}喜ばれます。"
+ )
+ ),
+ LessConsume -> FairyLore(
+ NonEmptyList.of(
+ s"$GREEN$UNDERLINE${BOLD}リンゴだいじに",
+ s"$RESET${GRAY}少しだけ妖精さんにりんごを開放します。",
+ s"$RESET${GRAY}伝えると大抵落ち込みます。"
+ )
+ ),
+ NoConsume -> FairyLore(
+ NonEmptyList.of(s"$BLUE$UNDERLINE${BOLD}リンゴつかうな", s"$RESET${GRAY}絶対にりんごを開放しません。", "")
+ )
+ )
+
+ RecomputedButton(for {
+ consumeStrategy <- fairyAPI.consumeStrategy(uuid)
+ } yield {
+ Button(
+ new IconItemStackBuilder(Material.PAPER)
+ .title(s"$GOLD$UNDERLINE${BOLD}妖精とのお約束")
+ .lore(fairyLoreTable(consumeStrategy).lore.toList)
+ .build(),
+ LeftClickButtonEffect {
+ SequentialEffect(
+ DeferredEffect(
+ IO(
+ fairyAPI.updateAppleOpenState(appleConsumeStrategyDependency(consumeStrategy))
+ )
+ ),
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f)
+ )
+ }
+ )
+ })
+ }
+
+ val fairyPlaySoundToggleButton: IO[Button] = {
+ val description =
+ List(s"$RESET$DARK_GRAY※この機能はデフォルトでONです。", s"$RESET$DARK_RED${UNDERLINE}クリックで切り替え")
+ val playSoundOnLore = s"$RESET${GREEN}現在音が鳴る設定になっています。" +: description
+ val playSoundOffLore = s"$RESET${RED}現在音が鳴らない設定になっています。" +: description
+
+ RecomputedButton(for {
+ fairySpeechSound <- fairySpeechAPI.playSoundOnSpeech(player.getUniqueId)
+ } yield {
+ Button(
+ new IconItemStackBuilder(Material.JUKEBOX)
+ .title(s"$GOLD$UNDERLINE${BOLD}マナ妖精の音トグル")
+ .lore(
+ if (fairySpeechSound) playSoundOnLore
+ else playSoundOffLore
+ )
+ .build(),
+ LeftClickButtonEffect {
+ SequentialEffect(
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f),
+ DeferredEffect(IO(fairySpeechAPI.togglePlaySoundOnSpeech))
+ )
+ }
+ )
+ })
+ }
+
+ val fairySummonButton: IO[Button] = for {
+ fairySummonState <- fairyAPI.fairySummonCost(player)
+ fairySummonRequestResult <- fairyAPI.fairySummonRequest(player)
+ } yield {
+ Button(
+ new IconItemStackBuilder(Material.GHAST_TEAR)
+ .title(s"$LIGHT_PURPLE$UNDERLINE${BOLD}マナ妖精 召喚")
+ .lore(
+ List(
+ s"$RESET$GRAY${fairySummonState.value * 2}投票ptを消費して",
+ s"$RESET${GRAY}マナ妖精を呼びます",
+ s"$RESET${GRAY}時間: ${fairySummonCostToString(fairySummonState)}",
+ s"$RESET${DARK_RED}Lv.10以上で開放"
+ )
+ )
+ .enchanted()
+ .build(),
+ LeftClickButtonEffect {
+ SequentialEffect(
+ fairySummonRequestResult match {
+ case Left(errorResult) =>
+ errorResult match {
+ case NotEnoughSeichiLevel =>
+ errorEffectOnSpawn(s"${GOLD}プレイヤーレベルが足りません")
+ case AlreadyFairySummoned =>
+ errorEffectOnSpawn(s"${GOLD}既に妖精を召喚しています")
+ case NotEnoughEffectPoint =>
+ errorEffectOnSpawn(s"${GOLD}投票ptが足りません")
+ }
+ case Right(process) =>
+ DeferredEffect {
+ for {
+ _ <- process
+ } yield emptyEffect
+ }
+ },
+ closeInventoryEffect
+ )
+ }
+ )
+ }
+
+ private def fairySummonCostToString(fairySummonCost: FairySummonCost): String = {
+ fairySummonCost.value match {
+ case 1 => "30分"
+ case 2 => "1時間"
+ case 3 => "1時間30分"
+ case 4 => "2時間"
+ }
+ }
+
+ private def errorEffectOnSpawn(message: String): Kleisli[IO, Player, Unit] = {
+ SequentialEffect(
+ MessageEffect(message),
+ FocusedSoundEffect(Sound.BLOCK_GLASS_PLACE, 1f, 0.1f)
+ )
+ }
+
+ val checkTimeButton: IO[Button] = IO {
+ Button(
+ new IconItemStackBuilder(Material.COMPASS)
+ .title(s"$LIGHT_PURPLE$UNDERLINE${BOLD}マナ妖精に時間を聞く")
+ .lore(List(s"$RESET${GRAY}妖精さんはいそがしい。", s"${GRAY}帰っちゃう時間を教えてくれる"))
+ .enchanted()
+ .build(),
+ LeftClickButtonEffect {
+ SequentialEffect(
+ DeferredEffect(IO(fairyAPI.sendDisappearTimeToChat)),
+ closeInventoryEffect
+ )
+ }
+ )
+ }
+
+ val gachaRingoInformation: IO[Button] = for {
+ myRank <- fairyAPI.rankByMostConsumedApple(player)
+ topFourRanking <- fairyAPI.rankingByMostConsumedApple(4)
+ allEatenAppleAmount <- fairyAPI.totalConsumedApple
+ } yield {
+ val staticLore = List(
+ s"$RESET$RED$BOLD※ニンゲンに見られないように気を付けること!",
+ s"$RESET$RED$BOLD 毎日大妖精からデータを更新すること!",
+ "",
+ s"$RESET$GOLD${BOLD}昨日までにがちゃりんごを",
+ s"$RESET$GOLD${BOLD}たくさんくれたニンゲンたち",
+ s"$RESET${DARK_GRAY}召喚されたらラッキーだよ!"
+ )
+ val topFourRankingLore =
+ List(
+ topFourRanking.headOption,
+ topFourRanking.lift(1),
+ topFourRanking.lift(2),
+ topFourRanking.lift(3)
+ ).flatMap(_.flatten).flatMap { rankData =>
+ List(
+ s"${GRAY}たくさんくれたニンゲン第${rankData.rank}位!",
+ s"${GRAY}なまえ:${rankData.playerName} りんご:${rankData.consumed.amount}個"
+ )
+ }
+ val statistics = myRank.map { rank =>
+ List(
+ s"${AQUA}ぜーんぶで${allEatenAppleAmount.amount}個もらえた!",
+ "",
+ s"$GREEN↓呼び出したニンゲンの情報↓",
+ s"${GREEN}今までに${rank.consumed.amount}個もらった",
+ s"${GREEN}ニンゲンの中では${rank.rank}番目にたくさんくれる!"
+ )
+ }.orEmpty
+
+ Button(
+ new IconItemStackBuilder(Material.GOLDEN_APPLE)
+ .title(s"$YELLOW$UNDERLINE$BOLD㊙ がちゃりんご情報 ㊙")
+ .lore(staticLore ++ topFourRankingLore ++ statistics)
+ .enchanted()
+ .build()
+ )
+ }
+
+ }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/achievement/AchievementMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/achievement/AchievementMenu.scala
index 6bc773881a..fe318872ad 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/achievement/AchievementMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/achievement/AchievementMenu.scala
@@ -9,12 +9,13 @@ import com.github.unchama.minecraft.actions.OnMinecraftServerThread
import com.github.unchama.seichiassist.SeichiAssist
import com.github.unchama.seichiassist.achievement.hierarchy.AchievementCategory
import com.github.unchama.seichiassist.achievement.hierarchy.AchievementCategory._
-import com.github.unchama.seichiassist.data.MenuInventoryData
import com.github.unchama.seichiassist.data.player.NicknameStyle
import com.github.unchama.seichiassist.effects.player.CommonSoundEffects
+import com.github.unchama.seichiassist.menus.nicknames.NickNameMenu
import com.github.unchama.seichiassist.menus.stickmenu.FirstPage
import com.github.unchama.seichiassist.menus.{ColorScheme, CommonButtons}
-import com.github.unchama.targetedeffect.player.{FocusedSoundEffect, PlayerEffects}
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.targetedeffect.player.FocusedSoundEffect
import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
import org.bukkit.ChatColor._
import org.bukkit.entity.Player
@@ -28,7 +29,9 @@ object AchievementMenu extends Menu {
class Environment(
implicit val ioCanOpenStickMenu: IO CanOpen FirstPage.type,
val ioCanOpenCategoryMenu: IO CanOpen AchievementCategoryMenu,
- val ioOnMainThread: OnMinecraftServerThread[IO]
+ val ioOnMainThread: OnMinecraftServerThread[IO],
+ val voteAPI: VoteAPI[IO, Player],
+ val ioCanOpenNickNameMenu: IO CanOpen NickNameMenu.type
)
override val frame: MenuFrame = MenuFrame(4.chestRows, s"$DARK_PURPLE${BOLD}実績・二つ名システム")
@@ -108,8 +111,10 @@ object AchievementMenu extends Menu {
.lore(s"${RED}設定画面を表示します。")
.build(),
action.LeftClickButtonEffect(
- CommonSoundEffects.menuTransitionFenceSound,
- PlayerEffects.openInventoryEffect(MenuInventoryData.computeRefreshedCombineMenu(player))
+ SequentialEffect(
+ CommonSoundEffects.menuTransitionFenceSound,
+ ioCanOpenNickNameMenu.open(NickNameMenu)
+ )
)
)
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/nicknames/NickNameMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/nicknames/NickNameMenu.scala
new file mode 100644
index 0000000000..79d30d4de4
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/nicknames/NickNameMenu.scala
@@ -0,0 +1,222 @@
+package com.github.unchama.seichiassist.menus.nicknames
+
+import cats.effect.{IO, SyncIO}
+import com.github.unchama.itemstackbuilder.{IconItemStackBuilder, SkullItemStackBuilder}
+import com.github.unchama.menuinventory.router.CanOpen
+import com.github.unchama.menuinventory.slot.button.{Button, RecomputedButton}
+import com.github.unchama.menuinventory.slot.button.action.LeftClickButtonEffect
+import com.github.unchama.menuinventory.syntax.IntInventorySizeOps
+import com.github.unchama.menuinventory.{
+ ChestSlotRef,
+ LayoutPreparationContext,
+ Menu,
+ MenuFrame,
+ MenuSlotLayout
+}
+import com.github.unchama.minecraft.actions.OnMinecraftServerThread
+import com.github.unchama.seichiassist.{SeichiAssist, SkullOwners}
+import com.github.unchama.seichiassist.achievement.Nicknames
+import com.github.unchama.seichiassist.data.MenuInventoryData
+import com.github.unchama.seichiassist.menus.CommonButtons
+import com.github.unchama.seichiassist.menus.achievement.AchievementMenu
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.targetedeffect.{SequentialEffect, UnfocusedEffect}
+import com.github.unchama.targetedeffect.player.FocusedSoundEffect
+import com.github.unchama.targetedeffect.player.PlayerEffects.openInventoryEffect
+import org.bukkit.{Material, Sound}
+import org.bukkit.entity.Player
+import org.bukkit.ChatColor._
+
+object NickNameMenu extends Menu {
+
+ class Environment(
+ implicit val ioCanOpenAchievementMenu: IO CanOpen AchievementMenu.type,
+ implicit val layoutPreparationContext: LayoutPreparationContext,
+ implicit val onMinecraftServerThread: OnMinecraftServerThread[IO],
+ implicit val voteAPI: VoteAPI[IO, Player]
+ )
+
+ override val frame: MenuFrame = MenuFrame(4.chestRows, s"$DARK_PURPLE${BOLD}二つ名組み合わせシステム")
+
+ import eu.timepit.refined.auto._
+
+ override def computeMenuLayout(
+ player: Player
+ )(implicit environment: Environment): IO[MenuSlotLayout] = {
+ import environment._
+
+ val backToAchievementMenu = CommonButtons.transferButton(
+ new SkullItemStackBuilder(SkullOwners.MHF_ArrowLeft),
+ s"$YELLOW$BOLD${UNDERLINE}実績・二つ名メニューへ",
+ AchievementMenu
+ )
+
+ val buttons = NickNameMenuButtons(player)
+ import buttons._
+
+ for {
+ achievementPointsInformation <- achievementPointsInformation
+ pointConvertButton <- pointConvertButton
+ } yield {
+ MenuSlotLayout(
+ ChestSlotRef(0, 0) -> achievementPointsInformation,
+ ChestSlotRef(0, 1) -> pointConvertButton,
+ ChestSlotRef(0, 4) -> currentNickName,
+ ChestSlotRef(1, 0) -> achievementPointShop,
+ ChestSlotRef(1, 2) -> headPartsSelect,
+ ChestSlotRef(1, 4) -> middlePartsSelect,
+ ChestSlotRef(1, 6) -> tailPartsSelect,
+ ChestSlotRef(3, 0) -> backToAchievementMenu
+ )
+ }
+ }
+
+ private case class NickNameMenuButtons(player: Player)(implicit environment: Environment) {
+
+ import environment._
+ import cats.implicits._
+
+ private val playerData = SeichiAssist.playermap.apply(player.getUniqueId)
+
+ val achievementPointsInformation: IO[Button] = RecomputedButton {
+ IO {
+ val itemStack = new IconItemStackBuilder(Material.EMERALD_ORE)
+ .title(s"$YELLOW$UNDERLINE${BOLD}実績ポイント情報")
+ .lore(
+ List(
+ s"${GREEN}クリックで情報を最新化",
+ s"${RED}累計獲得量:${playerData.achievePoint.cumulativeTotal}",
+ s"${RED}累計消費量:${playerData.achievePoint.used}",
+ s"${RED}使用可能量:${playerData.achievePoint.left}"
+ )
+ )
+ .build()
+
+ Button(
+ itemStack,
+ LeftClickButtonEffect {
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f, 1.0f)
+ }
+ )
+ }
+ }
+
+ val pointConvertButton: IO[Button] = RecomputedButton {
+ for {
+ effectPoint <- voteAPI.effectPoints(player).map(_.value)
+ itemStack = new IconItemStackBuilder(Material.EMERALD)
+ .title(s"$YELLOW$UNDERLINE${BOLD}ポイント変換ボタン")
+ .lore(
+ List(
+ s"${RED}JMS投票で手に入るポイントを",
+ s"${RED}実績ポイントに変換できます。",
+ s"$YELLOW${BOLD}投票pt 10pt → 実績pt 3pt",
+ s"${AQUA}クリックで変換を一回行います。",
+ s"${GREEN}所有投票pt:$effectPoint",
+ s"${GREEN}所有実績pt;${playerData.achievePoint.left}"
+ )
+ )
+ .build()
+ button = Button(
+ itemStack,
+ LeftClickButtonEffect {
+ UnfocusedEffect {
+ onMinecraftServerThread.runAction {
+ for {
+ _ <- SyncIO {
+ if (effectPoint >= 10)
+ playerData.convertEffectPointToAchievePoint
+ else player.sendMessage("エフェクトポイントが不足しています。")
+ }.whenA(effectPoint >= 10)
+ } yield ()
+ }
+ }
+ }
+ )
+ } yield button
+ }
+
+ val currentNickName: Button = {
+ val nickname = playerData.settings.nickname
+ val playerTitle =
+ Nicknames.getCombinedNicknameFor(nickname.id1, nickname.id2, nickname.id3).getOrElse("")
+
+ Button(
+ new IconItemStackBuilder(Material.BOOK)
+ .title(s"$YELLOW$UNDERLINE${BOLD}現在の二つ名の確認")
+ .lore(List(s"$RED「$playerTitle」"))
+ .build()
+ )
+ }
+
+ val achievementPointShop: Button = {
+ val itemStack = new IconItemStackBuilder(Material.ITEM_FRAME)
+ .title(s"$YELLOW$UNDERLINE${BOLD}実績ポイントショップ")
+ .lore(List(s"${GREEN}クリックで開きます"))
+ .build()
+
+ Button(
+ itemStack,
+ LeftClickButtonEffect {
+ SequentialEffect(
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f, 0.1f),
+ openInventoryEffect(MenuInventoryData.computePartsShopMenu(player))
+ )
+ }
+ )
+ }
+
+ val headPartsSelect: Button = {
+ val itemStack = new IconItemStackBuilder(Material.WATER_BUCKET)
+ .title(s"$YELLOW$UNDERLINE${BOLD}前パーツ選択画面")
+ .lore(List(s"${RED}クリックで移動します。"))
+ .build()
+
+ Button(
+ itemStack,
+ LeftClickButtonEffect {
+ SequentialEffect(
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f, 0.1f),
+ openInventoryEffect(MenuInventoryData.computeHeadPartCustomMenu(player))
+ )
+ }
+ )
+ }
+
+ val middlePartsSelect: Button = {
+ val itemStack = new IconItemStackBuilder(Material.MILK_BUCKET)
+ .title(s"$YELLOW$UNDERLINE${BOLD}中パーツ選択画面")
+ .lore(List(s"${RED}クリックで移動します"))
+ .build()
+
+ Button(
+ itemStack,
+ LeftClickButtonEffect {
+ SequentialEffect(
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f, 0.1f),
+ openInventoryEffect(MenuInventoryData.computeMiddlePartCustomMenu(player))
+ )
+ }
+ )
+ }
+
+ val tailPartsSelect: Button = {
+ val itemStack = new IconItemStackBuilder(Material.LAVA_BUCKET)
+ .title(s"$YELLOW$UNDERLINE${BOLD}後パーツ選択画面")
+ .lore(List(s"${RED}クリックで移動します。"))
+ .build()
+
+ Button(
+ itemStack,
+ LeftClickButtonEffect {
+ SequentialEffect(
+ FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1.0f, 0.1f),
+ openInventoryEffect(MenuInventoryData.computeTailPartCustomMenu(player))
+ )
+ }
+ )
+ }
+
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillEffectMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillEffectMenu.scala
index 5b8cbadc48..bcef47f214 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillEffectMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillEffectMenu.scala
@@ -16,6 +16,8 @@ import com.github.unchama.seichiassist.seichiskill.effect.{
ActiveSkillPremiumEffect,
UnlockableActiveSkillEffect
}
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.domain.EffectPoint
import com.github.unchama.seichiassist.subsystems.donate.DonatePremiumPointAPI
import com.github.unchama.seichiassist.{SeichiAssist, SkullOwners}
import com.github.unchama.targetedeffect.commandsender.MessageEffect
@@ -36,12 +38,13 @@ object ActiveSkillEffectMenu extends Menu {
val ioCanOpenActiveSkillMenu: IO CanOpen ActiveSkillMenu.type,
val ioCanOpenTransactionHistoryMenu: IO CanOpen PremiumPointTransactionHistoryMenu,
val ioOnMainThread: OnMinecraftServerThread[IO],
+ val voteAPI: VoteAPI[IO, Player],
val donateAPI: DonatePremiumPointAPI[IO]
)
override val frame: MenuFrame = MenuFrame(6.chestRows, s"$DARK_PURPLE${BOLD}整地スキルエフェクト選択")
- def setEffectSelectionTo(effect: ActiveSkillEffect)(player: Player): IO[Unit] = {
+ private def setEffectSelectionTo(effect: ActiveSkillEffect)(player: Player): IO[Unit] = {
val playerData = SeichiAssist.playermap(player.getUniqueId)
MessageEffect(s"${GREEN}エフェクト:${effect.nameOnUI} が選択されました")(player) >> IO {
@@ -49,27 +52,31 @@ object ActiveSkillEffectMenu extends Menu {
}
}
- def unlockOrSet(
- effect: ActiveSkillEffect
- )(implicit donateAPI: DonatePremiumPointAPI[IO]): TargetedEffect[Player] = Kleisli { player =>
+ private def unlockOrSet(effect: ActiveSkillEffect)(
+ implicit voteAPI: VoteAPI[IO, Player],
+ donateAPI: DonatePremiumPointAPI[IO]
+ ): TargetedEffect[Player] = Kleisli { player =>
val playerData = SeichiAssist.playermap(player.getUniqueId)
def unlockNormalEffect(effect: ActiveSkillNormalEffect): IO[Unit] =
for {
- effectPoint <- IO { playerData.effectPoint }
+ effectPoint <- voteAPI.effectPoints(player)
_ <-
- if (effectPoint < effect.usePoint) {
+ if (effectPoint.value < effect.usePoint) {
SequentialEffect(
MessageEffect(s"${DARK_RED}エフェクトポイントが足りません"),
FocusedSoundEffect(Sound.BLOCK_GLASS_PLACE, 1.0f, 0.5f)
).apply(player)
} else {
- IO {
- playerData.effectPoint -= effect.usePoint
- val state = playerData.skillEffectState
- playerData.skillEffectState =
- state.copy(obtainedEffects = state.obtainedEffects + effect)
- } >> SequentialEffect(
+ voteAPI.decreaseEffectPoint(
+ player.getUniqueId,
+ EffectPoint(effect.usePoint)
+ ) >> SequentialEffect(
+ UnfocusedEffect {
+ val state = playerData.skillEffectState
+ playerData.skillEffectState =
+ state.copy(obtainedEffects = state.obtainedEffects + effect)
+ },
MessageEffect(
s"${LIGHT_PURPLE}エフェクト:${effect.nameOnUI}$RESET$LIGHT_PURPLE${BOLD}を解除しました"
),
@@ -171,6 +178,7 @@ object ActiveSkillEffectMenu extends Menu {
val effectDataButton: IO[Button] =
for {
+ effectPoints <- voteAPI.effectPoints(player)
premiumEffectPoint <- donateAPI.currentPoint(player.getUniqueId)
button <-
IO {
@@ -183,7 +191,7 @@ object ActiveSkillEffectMenu extends Menu {
.lore(
List(
s"$RESET${GREEN}現在選択しているエフェクト:${playerData.skillEffectState.selection.nameOnUI}",
- s"$RESET${YELLOW}使えるエフェクトポイント:${playerData.effectPoint}",
+ s"$RESET${YELLOW}使えるエフェクトポイント:${effectPoints.value}",
s"$RESET$DARK_GRAY※投票すると獲得できます",
s"$RESET${LIGHT_PURPLE}使えるプレミアムポイント${premiumEffectPoint.value}",
s"$RESET$DARK_GRAY※寄付をすると獲得できます"
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala b/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala
index 3d83a40849..02262db2d1 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala
@@ -11,7 +11,6 @@ import com.github.unchama.menuinventory.slot.button.action.{
LeftClickButtonEffect
}
import com.github.unchama.menuinventory.slot.button.{Button, RecomputedButton, action}
-import com.github.unchama.seichiassist.data.MenuInventoryData
import com.github.unchama.seichiassist.data.descrptions.PlayerStatsLoreGenerator
import com.github.unchama.seichiassist.effects.player.CommonSoundEffects
import com.github.unchama.seichiassist.menus.achievement.AchievementMenu
@@ -19,7 +18,12 @@ import com.github.unchama.seichiassist.menus.home.HomeMenu
import com.github.unchama.seichiassist.menus.minestack.MineStackMainMenu
import com.github.unchama.seichiassist.menus.ranking.RankingRootMenu
import com.github.unchama.seichiassist.menus.skill.{ActiveSkillMenu, PassiveSkillMenu}
-import com.github.unchama.seichiassist.menus.{CommonButtons, RegionMenu, ServerSwitchMenu}
+import com.github.unchama.seichiassist.menus.{
+ CommonButtons,
+ RegionMenu,
+ ServerSwitchMenu,
+ VoteMenu
+}
import com.github.unchama.seichiassist.subsystems.anywhereender.AnywhereEnderChestAPI
import com.github.unchama.seichiassist.subsystems.anywhereender.domain.AccessDenialReason
import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountReadAPI
@@ -40,6 +44,7 @@ import com.github.unchama.seichiassist.subsystems.gachapoint.GachaPointApi
import com.github.unchama.seichiassist.subsystems.ranking.api.RankingProvider
import com.github.unchama.seichiassist.task.CoolDownTask
import com.github.unchama.seichiassist.ManagedWorld._
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
import com.github.unchama.seichiassist.{SeichiAssist, SkullOwners, util}
import com.github.unchama.targetedeffect.TargetedEffect.emptyEffect
import com.github.unchama.targetedeffect.commandsender.MessageEffect
@@ -86,8 +91,10 @@ object FirstPage extends Menu {
val ioCanOpenHomeMenu: IO CanOpen HomeMenu,
val ioCanOpenPassiveSkillMenu: IO CanOpen PassiveSkillMenu.type,
val ioCanOpenRankingRootMenu: IO CanOpen RankingRootMenu.type,
+ val ioCanOpenVoteMenu: IO CanOpen VoteMenu.type,
val enderChestAccessApi: AnywhereEnderChestAPI[IO],
- val gachaTicketAPI: GachaTicketAPI[IO]
+ val gachaTicketAPI: GachaTicketAPI[IO],
+ val voteAPI: VoteAPI[IO, Player]
)
override val frame: MenuFrame =
@@ -170,8 +177,13 @@ object FirstPage extends Menu {
environment.breakCountAPI.seichiAmountDataRepository(player).read.toIO
ranking <- environment.rankingApi.ranking.read
visibility <- visibilityRef.get.toIO
- lore <- new PlayerStatsLoreGenerator(openerData, ranking, seichiAmountData, visibility)
- .computeLore()
+ lore <- new PlayerStatsLoreGenerator(
+ openerData,
+ ranking,
+ seichiAmountData,
+ visibility,
+ environment.voteAPI
+ ).computeLore()
} yield Button(
new SkullItemStackBuilder(getUniqueId)
.title(s"$YELLOW$BOLD$UNDERLINE${getName}の統計データ")
@@ -737,7 +749,7 @@ object FirstPage extends Menu {
)
}
- val votePointMenuButton: Button = {
+ def votePointMenuButton(implicit ioCanOpenVoteMenu: IO CanOpen VoteMenu.type): Button = {
val iconItemStack =
new IconItemStackBuilder(Material.DIAMOND)
.enchanted()
@@ -749,8 +761,7 @@ object FirstPage extends Menu {
iconItemStack,
LeftClickButtonEffect(
CommonSoundEffects.menuTransitionFenceSound,
- // TODO メニューに置き換える
- ComputedEffect(p => openInventoryEffect(MenuInventoryData.getVotingMenuData(p)))
+ ioCanOpenVoteMenu.open(VoteMenu)
)
)
}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/gachaprize/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/gachaprize/System.scala
index 163077308f..c8998691f2 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/gachaprize/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/gachaprize/System.scala
@@ -57,7 +57,7 @@ object System {
val system: F[System[F]] = for {
persistedGachaPrizes <- _gachaPersistence.list
expBottle = GachaPrize(
- new ItemStack(Material.EXP_BOTTLE, 1),
+ new ItemStack(Material.EXP_BOTTLE, 20),
GachaProbability(0.1),
signOwner = false,
GachaPrizeId(2),
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/bukkit/commands/LastQuitCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/bukkit/commands/LastQuitCommand.scala
index 44b55ea9d8..d6bbe2eff1 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/bukkit/commands/LastQuitCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/lastquit/bukkit/commands/LastQuitCommand.scala
@@ -3,11 +3,11 @@ package com.github.unchama.seichiassist.subsystems.lastquit.bukkit.commands
import cats.effect.ConcurrentEffect.ops.toAllConcurrentEffectOps
import cats.effect.{ConcurrentEffect, IO}
import com.github.unchama.contextualexecutor.builder.{ContextualExecutorBuilder, Parsers}
-import com.github.unchama.seichiassist.subsystems.lastquit.LastQuitAPI
-import com.github.unchama.seichiassist.subsystems.lastquit.infrastructure.{
+import com.github.unchama.seichiassist.infrastructure.minecraft.{
JdbcLastSeenNameToUuid,
LastSeenNameToUuidError
}
+import com.github.unchama.seichiassist.subsystems.lastquit.LastQuitAPI
import com.github.unchama.targetedeffect.commandsender.MessageEffect
import org.bukkit.ChatColor.RED
import org.bukkit.command.TabExecutor
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/codec/BukkitMebiusItemStackCodec.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/codec/BukkitMebiusItemStackCodec.scala
index 3a10d9bd02..226a5afa8e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/codec/BukkitMebiusItemStackCodec.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/codec/BukkitMebiusItemStackCodec.scala
@@ -56,12 +56,18 @@ object BukkitMebiusItemStackCodec {
def encodeForcedMaterial(forcedMaterial: MebiusForcedMaterial): Byte = forcedMaterial match {
case MebiusForcedMaterial.None => 0
case MebiusForcedMaterial.Leather => 1
+ case MebiusForcedMaterial.Iron => 2
+ case MebiusForcedMaterial.Chain => 3
+ case MebiusForcedMaterial.Gold => 4
}
def decodeForcedMaterial(forcedMaterialByte: Byte): MebiusForcedMaterial =
forcedMaterialByte match {
case 0 => MebiusForcedMaterial.None
case 1 => MebiusForcedMaterial.Leather
+ case 2 => MebiusForcedMaterial.Iron
+ case 3 => MebiusForcedMaterial.Chain
+ case 4 => MebiusForcedMaterial.Gold
case _ => MebiusForcedMaterial.None
}
@@ -117,7 +123,10 @@ object BukkitMebiusItemStackCodec {
val material = property.forcedMaterial match {
case MebiusForcedMaterial.None =>
BukkitMebiusAppearanceMaterialCodec.appearanceMaterialAt(property.level)
- case MebiusForcedMaterial.Leather => Material.LEATHER_HELMET
+ case MebiusForcedMaterial.Leather => Material.LEATHER_HELMET // 革のヘルメット
+ case MebiusForcedMaterial.Iron => Material.IRON_HELMET // 鉄のヘルメット
+ case MebiusForcedMaterial.Chain => Material.CHAINMAIL_HELMET // チェーンのヘルメット
+ case MebiusForcedMaterial.Gold => Material.GOLD_HELMET // 金のヘルメット
}
import scala.util.chaining._
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/command/MebiusCommandExecutorProvider.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/command/MebiusCommandExecutorProvider.scala
index 08e5ad679e..9fad985635 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/command/MebiusCommandExecutorProvider.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/bukkit/command/MebiusCommandExecutorProvider.scala
@@ -126,6 +126,9 @@ class MebiusCommandExecutorProvider(
val newMaterialName = newProperty.forcedMaterial match {
case MebiusForcedMaterial.None => "ダイヤモンド"
case MebiusForcedMaterial.Leather => "革"
+ case MebiusForcedMaterial.Gold => "金"
+ case MebiusForcedMaterial.Iron => "鉄"
+ case MebiusForcedMaterial.Chain => "チェーン"
}
IO.pure {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/domain/property/MebiusForcedMaterial.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/domain/property/MebiusForcedMaterial.scala
index 965ddaf463..0c06d6c347 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/domain/property/MebiusForcedMaterial.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/mebius/domain/property/MebiusForcedMaterial.scala
@@ -2,8 +2,7 @@ package com.github.unchama.seichiassist.subsystems.mebius.domain.property
/**
* Mebiusの材質を強制的に変更する設定。
- *
- * 「30レベル以上でのみ革に戻すことができる」という機能のため。
+ * 『30レベル以上で革や鉄、金、チェーンなどの素材に変更できる』機能のため。
*/
sealed trait MebiusForcedMaterial {
@@ -16,13 +15,49 @@ sealed trait MebiusForcedMaterial {
object MebiusForcedMaterial {
import cats.implicits._
+ // ダイヤモンド
case object None extends MebiusForcedMaterial {
+
override def allowedAt(level: MebiusLevel): Boolean = true
+
override def next: MebiusForcedMaterial = Leather
+
}
+ // 革
case object Leather extends MebiusForcedMaterial {
+
override def allowedAt(level: MebiusLevel): Boolean = level >= MebiusLevel(30)
+
+ override def next: MebiusForcedMaterial = Gold
+
+ }
+
+ // 金
+ case object Gold extends MebiusForcedMaterial {
+
+ override def allowedAt(level: MebiusLevel): Boolean = level >= MebiusLevel(30)
+
+ override def next: MebiusForcedMaterial = Iron
+
+ }
+
+ // 鉄
+ case object Iron extends MebiusForcedMaterial {
+
+ override def allowedAt(level: MebiusLevel): Boolean = level >= MebiusLevel(30)
+
+ override def next: MebiusForcedMaterial = Chain
+
+ }
+
+ // チェーン
+ case object Chain extends MebiusForcedMaterial {
+
+ override def allowedAt(level: MebiusLevel): Boolean = level >= MebiusLevel(30)
+
override def next: MebiusForcedMaterial = None
+
}
+
}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/infrastructure/JdbcVoteRankingRecordPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/infrastructure/JdbcVoteRankingRecordPersistence.scala
index 3012680e3c..2a06e54cc6 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/infrastructure/JdbcVoteRankingRecordPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/infrastructure/JdbcVoteRankingRecordPersistence.scala
@@ -12,8 +12,8 @@ class JdbcVoteRankingRecordPersistence[F[_]: Sync]
extends RankingRecordPersistence[F, VoteCount] {
override def getAllRankingRecords: F[Vector[RankingRecord[VoteCount]]] = Sync[F].delay {
DB.readOnly { implicit session =>
- sql"SELECT name,p_vote from playerdata"
- .map { rs => RankingRecord(rs.string("name"), VoteCount(rs.int("p_vote"))) }
+ sql"SELECT playerdata.name,vote_number FROM vote INNER JOIN playerdata ON vote.uuid = playerdata.uuid"
+ .map { rs => RankingRecord(rs.string("name"), VoteCount(rs.int("vote_number"))) }
.list()
.apply()
.toVector
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala
index 39a448dd55..205bd28943 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala
@@ -7,6 +7,7 @@ import com.github.unchama.seichiassist.subsystems.seasonalevents.anniversary.Ann
import com.github.unchama.seichiassist.subsystems.seasonalevents.christmas.ChristmasItemData._
import com.github.unchama.seichiassist.subsystems.seasonalevents.halloween.HalloweenItemData._
import com.github.unchama.seichiassist.subsystems.seasonalevents.newyear.NewYearItemData._
+import com.github.unchama.seichiassist.subsystems.seasonalevents.valentine.ValentineItemData._
import com.github.unchama.seichiassist.util.InventoryOperations
import com.github.unchama.targetedeffect.TargetedEffect._
import org.bukkit.command.TabExecutor
@@ -39,6 +40,9 @@ class EventCommand(implicit ioOnMainThread: OnMinecraftServerThread[IO]) {
anniversaryShovel
)
+ val valentineGrantEffect: TargetedEffect[Player] =
+ InventoryOperations.grantItemStacksEffect(droppedCookie)
+
val executor: TabExecutor = playerCommandBuilder
.execution { context =>
val effect = context.args.yetToBeParsed match {
@@ -46,6 +50,7 @@ class EventCommand(implicit ioOnMainThread: OnMinecraftServerThread[IO]) {
case "christmas" :: _ => christsmasGrantEffect
case "newyear" :: _ => newYearGrantEffect
case "halloween" :: _ => halloweenGrantEffect
+ case "valentine" :: _ => valentineGrantEffect
case _ => emptyEffect
}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala
index db1078b250..80a4411381 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala
@@ -25,7 +25,9 @@ object ValentineItemData {
s"${AQUA}ステータス変化(10分)$GRAY (期限内)"
).map(str => s"$RESET$str")
- private val cookieName = s"$GOLD${BOLD}チョコチップクッキー"
+ // 配布のチョコチップクッキーと区別できるように
+ // ref: https://github.com/GiganticMinecraft/SeichiAssist/issues/1910
+ private val cookieName = s"$GOLD${BOLD}爆発したカップルの本命チョコチップクッキー"
/**
* チョコチップクッキーであるかどうかを返す
@@ -83,10 +85,10 @@ object ValentineItemData {
// endregion
- // region GiftedCookie -> 棒メニューでもらえるやつ
+ // region GiftedCookie -> イベント期間中にログイン時に入手できる
/**
- * 棒メニューからチョコチップクッキーのアイテムID。2は有効期限が[[java.time.LocalDate]]のもの
+ * イベント期間中のログインでもらえるチョコチップクッキーのアイテムID。2は有効期限が[[java.time.LocalDate]]のもの
*/
private val giftedCookieTypeId = 4
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/sharedinventory/bukkit/command/ShareInventoryCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/sharedinventory/bukkit/command/ShareInventoryCommand.scala
index befec2347a..0fa77c95a9 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/sharedinventory/bukkit/command/ShareInventoryCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/sharedinventory/bukkit/command/ShareInventoryCommand.scala
@@ -8,6 +8,7 @@ import com.github.unchama.seichiassist.subsystems.sharedinventory.SharedInventor
import com.github.unchama.seichiassist.subsystems.sharedinventory.domain.SharedFlag
import com.github.unchama.seichiassist.subsystems.sharedinventory.domain.bukkit.InventoryContents
import com.github.unchama.seichiassist.util.InventoryOperations
+import com.github.unchama.targetedeffect.TargetedEffect.emptyEffect
import com.github.unchama.targetedeffect.commandsender.MessageEffect
import com.github.unchama.targetedeffect.player.CommandEffect
import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
@@ -21,20 +22,24 @@ class ShareInventoryCommand[F[_]: ConcurrentEffect](
implicit sharedInventoryAPI: SharedInventoryAPI[F, Player]
) {
+ import cats.implicits._
+
val executor: TabExecutor = playerCommandBuilder
.execution { context =>
val sender = context.sender
-
- if (sharedInventoryAPI.sharedFlag(sender).toIO.unsafeRunSync() == SharedFlag.Sharing)
- withdrawFromSharedInventory(sender)
- else depositToSharedInventory(sender)
-
+ for {
+ sharedFlag <- sharedInventoryAPI.sharedFlag(sender).toIO
+ _ <-
+ if (sharedFlag == SharedFlag.Sharing) {
+ withdrawFromSharedInventory(sender)
+ } else {
+ depositToSharedInventory(sender)
+ }
+ } yield emptyEffect
}
.build()
.asNonBlockingTabExecutor()
- import cats.implicits._
-
private def withdrawFromSharedInventory(player: Player): IO[TargetedEffect[Player]] = {
val uuid = player.getUniqueId
val eff = for {
@@ -42,12 +47,10 @@ class ShareInventoryCommand[F[_]: ConcurrentEffect](
loadedInventory <- sharedInventoryAPI.load(uuid)
_ <- sharedInventoryAPI.clear(uuid)
newSharedFlag <- sharedInventoryAPI.sharedFlag(player)
- playerInventory = player.getInventory
_ <- Sync[F]
.delay {
- val inventoryContents = loadedInventory
- .getOrElse(return IO.pure(MessageEffect(s"$RESET$RED${BOLD}収納アイテムが存在しません。")))
- .inventoryContents
+ val playerInventory = player.getInventory
+ val inventoryContents = loadedInventory.get.inventoryContents
// 手持ちのアイテムをドロップする
playerInventory
.getContents
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/System.scala
new file mode 100644
index 0000000000..143aded858
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/System.scala
@@ -0,0 +1,63 @@
+package com.github.unchama.seichiassist.subsystems.vote
+
+import cats.data.Kleisli
+import cats.effect.{ConcurrentEffect, SyncEffect}
+import com.github.unchama.minecraft.actions.OnMinecraftServerThread
+import com.github.unchama.seichiassist.meta.subsystem.Subsystem
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
+import com.github.unchama.seichiassist.subsystems.vote.application.actions.ReceiveVoteBenefits
+import com.github.unchama.seichiassist.subsystems.vote.bukkit.actions.BukkitReceiveVoteBenefits
+import com.github.unchama.seichiassist.subsystems.vote.bukkit.command.VoteCommand
+import com.github.unchama.seichiassist.subsystems.vote.bukkit.listeners.PlayerDataCreator
+import com.github.unchama.seichiassist.subsystems.vote.domain._
+import com.github.unchama.seichiassist.subsystems.vote.infrastructure.JdbcVotePersistence
+import org.bukkit.command.TabExecutor
+import org.bukkit.entity.Player
+import org.bukkit.event.Listener
+
+import java.util.UUID
+
+trait System[F[_], Player] extends Subsystem[F] {
+ val api: VoteAPI[F, Player]
+}
+
+object System {
+
+ def wired[F[_]: ConcurrentEffect: OnMinecraftServerThread, G[_]: SyncEffect](
+ implicit breakCountAPI: BreakCountAPI[F, G, Player]
+ ): System[F, Player] = {
+ implicit val _votePersistence: VotePersistence[F] = new JdbcVotePersistence[F]
+ val _receiveVoteBenefits: ReceiveVoteBenefits[F, Player] =
+ new BukkitReceiveVoteBenefits[F, G]()
+
+ new System[F, Player] {
+ override implicit val api: VoteAPI[F, Player] = new VoteAPI[F, Player] {
+ override def count(uuid: UUID): F[VoteCount] =
+ _votePersistence.currentVoteCount(uuid)
+
+ override def currentConsecutiveVoteStreakDays(uuid: UUID): F[ChainVoteDayNumber] =
+ _votePersistence.currentConsecutiveVoteStreakDay(uuid)
+
+ override def decreaseEffectPoint(uuid: UUID, effectPoint: EffectPoint): F[Unit] =
+ _votePersistence.decreaseEffectPoints(uuid, effectPoint)
+
+ override def effectPoints(player: Player): F[EffectPoint] =
+ _votePersistence.effectPoints(player.getUniqueId)
+
+ override def receivedVoteBenefits(uuid: UUID): F[VoteBenefit] =
+ _votePersistence.receivedVoteBenefits(uuid)
+
+ override def receiveVoteBenefits: Kleisli[F, Player, Unit] = Kleisli { player =>
+ _receiveVoteBenefits.receive(player)
+ }
+ }
+
+ override val commands: Map[String, TabExecutor] = Map(
+ "vote" -> new VoteCommand[F].executor
+ )
+
+ override val listeners: Seq[Listener] = Seq(new PlayerDataCreator[F])
+ }
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/VoteAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/VoteAPI.scala
new file mode 100644
index 0000000000..866ee3b7a2
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/VoteAPI.scala
@@ -0,0 +1,72 @@
+package com.github.unchama.seichiassist.subsystems.vote
+
+import cats.data.Kleisli
+import com.github.unchama.seichiassist.subsystems.vote.domain._
+
+import java.util.UUID
+
+trait VoteWriteAPI[F[_]] {
+
+ /**
+ * @return effectPointを減少させる作用
+ */
+ def decreaseEffectPoint(uuid: UUID, effectPoint: EffectPoint): F[Unit]
+
+}
+
+object VoteWriteAPI {
+
+ def apply[F[_]](implicit ev: VoteWriteAPI[F]): VoteWriteAPI[F] = ev
+
+}
+
+trait VoteReadAPI[F[_], Player] {
+
+ /**
+ * @return 投票回数を返す作用
+ */
+ def count(uuid: UUID): F[VoteCount]
+
+ /**
+ * @return 連続投票日数を返す作用
+ */
+ def currentConsecutiveVoteStreakDays(uuid: UUID): F[ChainVoteDayNumber]
+
+ /**
+ * @return effectPointを返す作用
+ */
+ def effectPoints(player: Player): F[EffectPoint]
+
+ /**
+ * @return 投票特典を受け取った回数を返す作用
+ */
+ def receivedVoteBenefits(uuid: UUID): F[VoteBenefit]
+
+}
+
+object VoteReadAPI {
+
+ def apply[F[_], Player](implicit ev: VoteReadAPI[F, Player]): VoteReadAPI[F, Player] = ev
+
+}
+
+trait VoteReceiveAPI[F[_], Player] {
+
+ /**
+ * @return 投票特典を受け取る作用
+ */
+ def receiveVoteBenefits: Kleisli[F, Player, Unit]
+
+}
+
+object VoteReceiveAPI {
+
+ def apply[F[_], Player](implicit ev: VoteReceiveAPI[F, Player]): VoteReceiveAPI[F, Player] =
+ ev
+
+}
+
+trait VoteAPI[F[_], Player]
+ extends VoteReadAPI[F, Player]
+ with VoteWriteAPI[F]
+ with VoteReceiveAPI[F, Player]
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/application/actions/ReceiveVoteBenefits.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/application/actions/ReceiveVoteBenefits.scala
new file mode 100644
index 0000000000..2ffd4c25f4
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/application/actions/ReceiveVoteBenefits.scala
@@ -0,0 +1,18 @@
+package com.github.unchama.seichiassist.subsystems.vote.application.actions
+
+trait ReceiveVoteBenefits[F[_], Player] {
+
+ /**
+ * @return 投票特典を受け取る作用
+ */
+ def receive(player: Player): F[Unit]
+
+}
+
+object ReceiveVoteBenefits {
+
+ def apply[F[_], Player](
+ implicit ev: ReceiveVoteBenefits[F, Player]
+ ): ReceiveVoteBenefits[F, Player] = ev
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/actions/BukkitReceiveVoteBenefits.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/actions/BukkitReceiveVoteBenefits.scala
new file mode 100644
index 0000000000..3ae4cae002
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/actions/BukkitReceiveVoteBenefits.scala
@@ -0,0 +1,52 @@
+package com.github.unchama.seichiassist.subsystems.vote.bukkit.actions
+
+import cats.effect.{Sync, SyncEffect}
+import com.github.unchama.generic.ContextCoercion
+import com.github.unchama.minecraft.actions.OnMinecraftServerThread
+import com.github.unchama.seichiassist.data.ItemData
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
+import com.github.unchama.seichiassist.subsystems.gachaprize.bukkit.factories.BukkitGachaSkullData
+import com.github.unchama.seichiassist.subsystems.vote.application.actions.ReceiveVoteBenefits
+import com.github.unchama.seichiassist.subsystems.vote.domain.{
+ EffectPoint,
+ VoteBenefit,
+ VotePersistence
+}
+import com.github.unchama.seichiassist.util.InventoryOperations.grantItemStacksEffect
+import org.bukkit.entity.Player
+
+class BukkitReceiveVoteBenefits[F[_]: OnMinecraftServerThread: Sync, G[
+ _
+]: SyncEffect: ContextCoercion[*[_], F]](
+ implicit votePersistence: VotePersistence[F],
+ breakCountAPI: BreakCountAPI[F, G, Player]
+) extends ReceiveVoteBenefits[F, Player] {
+
+ import cats.implicits._
+
+ override def receive(player: Player): F[Unit] = {
+ val uuid = player.getUniqueId
+ for {
+ totalVote <- votePersistence.currentVoteCount(uuid)
+ receivedVote <- votePersistence.receivedVoteBenefits(uuid)
+ pendingCount = VoteBenefit(totalVote.value - receivedVote.value)
+ // 受け取ってない分を受け取ったことにする
+ _ <- votePersistence.increaseVoteBenefits(uuid, pendingCount)
+ playerLevel <- ContextCoercion(breakCountAPI.seichiAmountDataRepository(player).read.map {
+ _.levelCorrespondingToExp.level
+ })
+ gachaTicketAmount = Seq.fill(10 * pendingCount.value)(BukkitGachaSkullData.gachaForVoting)
+ additionalVoteBenefit = Seq.fill(pendingCount.value)(
+ if (playerLevel < 50) ItemData.getSuperPickaxe(1)
+ else ItemData.getVotingGift(1)
+ )
+ grantItems = gachaTicketAmount ++ additionalVoteBenefit
+ _ <- {
+ ContextCoercion(votePersistence.increaseEffectPoints(uuid, EffectPoint(10)))
+ .replicateA(pendingCount.value) >>
+ grantItemStacksEffect[F](grantItems: _*).apply(player)
+ }.whenA(pendingCount.value != 0)
+ } yield ()
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/command/VoteCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/command/VoteCommand.scala
new file mode 100644
index 0000000000..d090463c6c
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/command/VoteCommand.scala
@@ -0,0 +1,64 @@
+package com.github.unchama.seichiassist.subsystems.vote.bukkit.command
+
+import cats.effect.ConcurrentEffect
+import cats.effect.ConcurrentEffect.ops.toAllConcurrentEffectOps
+import com.github.unchama.contextualexecutor.builder.ContextualExecutorBuilder
+import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
+import com.github.unchama.seichiassist.infrastructure.minecraft.{
+ JdbcLastSeenNameToUuid,
+ LastSeenNameToUuidError
+}
+import com.github.unchama.seichiassist.subsystems.vote.domain.VotePersistence
+import com.github.unchama.targetedeffect.commandsender.MessageEffect
+import com.github.unchama.targetedeffect.{DeferredEffect, SequentialEffect}
+import org.bukkit.ChatColor._
+import org.bukkit.command.TabExecutor
+
+class VoteCommand[F[_]: ConcurrentEffect](implicit votePersistence: VotePersistence[F]) {
+
+ private val usageEchoExecutor: EchoExecutor = EchoExecutor(
+ MessageEffect(List(s"$RED/vote record <プレイヤー名>", "投票特典配布用コマンドです"))
+ )
+
+ import cats.implicits._
+
+ private val recordExecutor = {
+ ContextualExecutorBuilder
+ .beginConfiguration()
+ .executionCSEffect { context =>
+ val playerName = context.args.yetToBeParsed.head
+ val distributionProcess = for {
+ uuidEither <- new JdbcLastSeenNameToUuid[F].of(playerName)
+ program <- uuidEither.traverse { uuid =>
+ votePersistence.incrementVoteCount(uuid) >> votePersistence
+ .updateConsecutiveVoteStreak(uuid)
+ }
+ } yield program match {
+ case Left(error) =>
+ error match {
+ case LastSeenNameToUuidError.MultipleFound =>
+ MessageEffect(s"${RED}指定された名前のプレイヤーが複数見つかりました。")
+ case LastSeenNameToUuidError.NotFound =>
+ MessageEffect(s"${RED}指定された名前のプレイヤーが見つかりませんでした。")
+ }
+ case Right(_) =>
+ MessageEffect(s"$AQUA${playerName}への特典配布処理に成功しました。")
+ }
+
+ SequentialEffect(
+ MessageEffect(s"$YELLOW${playerName}の特典配布処理開始…"),
+ DeferredEffect(distributionProcess.toIO)
+ )
+ }
+ .build()
+ }
+
+ val executor: TabExecutor = {
+ BranchedExecutor(
+ Map("record" -> recordExecutor),
+ whenBranchNotFound = Some(usageEchoExecutor),
+ whenArgInsufficient = Some(usageEchoExecutor)
+ )
+ }.asNonBlockingTabExecutor()
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/listeners/PlayerDataCreator.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/listeners/PlayerDataCreator.scala
new file mode 100644
index 0000000000..91ec918ba0
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/bukkit/listeners/PlayerDataCreator.scala
@@ -0,0 +1,17 @@
+package com.github.unchama.seichiassist.subsystems.vote.bukkit.listeners
+
+import cats.effect.ConcurrentEffect
+import cats.effect.ConcurrentEffect.ops.toAllConcurrentEffectOps
+import com.github.unchama.seichiassist.subsystems.vote.domain.VotePersistence
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent
+import org.bukkit.event.{EventHandler, EventPriority, Listener}
+
+class PlayerDataCreator[F[_]: ConcurrentEffect](implicit votePersistence: VotePersistence[F])
+ extends Listener {
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ def onPlayerPreLoginEvent(e: AsyncPlayerPreLoginEvent): Unit = {
+ votePersistence.createPlayerData(e.getUniqueId).toIO.unsafeRunAsyncAndForget()
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/ChainVoteDayNumber.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/ChainVoteDayNumber.scala
new file mode 100644
index 0000000000..7d4bf5ad23
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/ChainVoteDayNumber.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.domain
+
+case class ChainVoteDayNumber(value: Int) {
+ require(value >= 0, "投票日数は非負である必要があります。")
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/EffectPoint.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/EffectPoint.scala
new file mode 100644
index 0000000000..741a797b5a
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/EffectPoint.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.domain
+
+case class EffectPoint(value: Int) {
+ require(value >= 0, "EffectPointは非負である必要があります。")
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VoteBenefit.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VoteBenefit.scala
new file mode 100644
index 0000000000..ae6c7d4c0f
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VoteBenefit.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.domain
+
+case class VoteBenefit(value: Int) {
+ require(value >= 0, "VoteBenefitは非負である必要があります。")
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VoteCount.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VoteCount.scala
new file mode 100644
index 0000000000..fa56a30deb
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VoteCount.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.domain
+
+case class VoteCount(value: Int) {
+ require(value >= 0, "VoteCountは非負である必要があります。")
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VotePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VotePersistence.scala
new file mode 100644
index 0000000000..480de89fda
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/domain/VotePersistence.scala
@@ -0,0 +1,59 @@
+package com.github.unchama.seichiassist.subsystems.vote.domain
+
+import java.util.UUID
+
+trait VotePersistence[F[_]] {
+
+ /**
+ * プレイヤーの投票データを初期化します。この作用はプレイヤーがサーバーに参加するたびに呼ばれるため、作用が冪等でなければなりません。
+ *
+ * @return プレイヤーデータを作成する作用
+ */
+ def createPlayerData(uuid: UUID): F[Unit]
+
+ /**
+ * @return 投票回数をインクリメントする作用
+ */
+ def incrementVoteCount(uuid: UUID): F[Unit]
+
+ /**
+ * @return 投票回数を返す作用
+ */
+ def currentVoteCount(uuid: UUID): F[VoteCount]
+
+ /**
+ * @return 連続投票回数を更新する作用
+ */
+ def updateConsecutiveVoteStreak(uuid: UUID): F[Unit]
+
+ /**
+ * @return 連続投票日数を返す作用
+ */
+ def currentConsecutiveVoteStreakDay(uuid: UUID): F[ChainVoteDayNumber]
+
+ /**
+ * @return [[EffectPoint]]を指定分だけ増加させる作用
+ */
+ def increaseEffectPoints(uuid: UUID, effectPoint: EffectPoint): F[Unit]
+
+ /**
+ * @return [[EffectPoint]]を減少させる作用
+ */
+ def decreaseEffectPoints(uuid: UUID, effectPoint: EffectPoint): F[Unit]
+
+ /**
+ * @return 指定プレイヤーの[[EffectPoint]]を返す作用
+ */
+ def effectPoints(uuid: UUID): F[EffectPoint]
+
+ /**
+ * @return 投票特典を受け取った回数を増加させる作用
+ */
+ def increaseVoteBenefits(uuid: UUID, amount: VoteBenefit): F[Unit]
+
+ /**
+ * @return 投票特典を受け取った回数を返す作用
+ */
+ def receivedVoteBenefits(uuid: UUID): F[VoteBenefit]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/infrastructure/JdbcVotePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/infrastructure/JdbcVotePersistence.scala
new file mode 100644
index 0000000000..35d8b6644a
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/infrastructure/JdbcVotePersistence.scala
@@ -0,0 +1,121 @@
+package com.github.unchama.seichiassist.subsystems.vote.infrastructure
+
+import cats.effect.Sync
+import com.github.unchama.seichiassist.subsystems.vote.domain._
+import scalikejdbc.{DB, scalikejdbcSQLInterpolationImplicitDef}
+
+import java.util.UUID
+
+class JdbcVotePersistence[F[_]: Sync] extends VotePersistence[F] {
+
+ // NOTE: 連続投票日数許容幅を変更する場合はここを変更してください。
+ private val consecutiveVoteStreakDaysThreshold = 1
+
+ override def createPlayerData(uuid: UUID): F[Unit] = Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"""INSERT IGNORE INTO vote
+ | (uuid, vote_number, chain_vote_number, effect_point, given_effect_point, last_vote)
+ | VALUES
+ | (${uuid.toString}, 0, 0, 0, 0, NULL)""".stripMargin.execute().apply()
+ }
+ }
+
+ override def incrementVoteCount(uuid: UUID): F[Unit] = Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote SET vote_number = vote_number + 1 WHERE uuid = (SELECT uuid FROM playerdata WHERE uuid = ${uuid.toString})"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def currentVoteCount(uuid: UUID): F[VoteCount] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val votePoint = sql"SELECT vote_number FROM vote WHERE uuid = ${uuid.toString}"
+ .map(_.int("vote_number"))
+ .single()
+ .apply()
+ .get
+ VoteCount(votePoint)
+ }
+ }
+
+ override def updateConsecutiveVoteStreak(uuid: UUID): F[Unit] = Sync[F].delay {
+ DB.localTx { implicit session =>
+ /*
+ NOTE: 最終投票日時より(連続投票許容幅 - 1)した日時よりも
+ 小さかった場合に連続投票を0に戻します。
+ */
+ sql"""UPDATE vote SET chain_vote_number =
+ | CASE WHEN DATEDIFF(last_vote, NOW()) <= ${-consecutiveVoteStreakDaysThreshold - 1}
+ | THEN 0
+ | ELSE chain_vote_number + 1
+ | END,
+ | last_vote = NOW()
+ | WHERE uuid = (SELECT uuid FROM playerdata WHERE uuid = ${uuid.toString})"""
+ .stripMargin
+ .execute()
+ .apply()
+ }
+ }
+
+ override def currentConsecutiveVoteStreakDay(uuid: UUID): F[ChainVoteDayNumber] =
+ Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val chainVoteDays =
+ sql"SELECT chain_vote_number FROM vote WHERE uuid = ${uuid.toString}"
+ .map(_.int("chain_vote_number"))
+ .single()
+ .apply()
+ .get
+ ChainVoteDayNumber(chainVoteDays)
+ }
+ }
+
+ override def increaseEffectPoints(uuid: UUID, effectPoint: EffectPoint): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote SET effect_point = effect_point + ${effectPoint.value} WHERE uuid = ${uuid.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def decreaseEffectPoints(uuid: UUID, effectPoint: EffectPoint): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote SET effect_point = effect_point - ${effectPoint.value} WHERE uuid = ${uuid.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def effectPoints(uuid: UUID): F[EffectPoint] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val effectPoints = sql"SELECT effect_point FROM vote WHERE uuid = ${uuid.toString}"
+ .map(_.int("effect_point"))
+ .single()
+ .apply()
+ .get
+ EffectPoint(effectPoints)
+ }
+ }
+
+ override def increaseVoteBenefits(uuid: UUID, benefit: VoteBenefit): F[Unit] = Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote SET given_effect_point = given_effect_point + ${benefit.value} WHERE uuid = ${uuid.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def receivedVoteBenefits(uuid: UUID): F[VoteBenefit] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val benefits = sql"SELECT given_effect_point FROM vote WHERE uuid = ${uuid.toString}"
+ .map(_.int("given_effect_point"))
+ .single()
+ .apply()
+ .get
+ VoteBenefit(benefits)
+ }
+ }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/FairyAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/FairyAPI.scala
new file mode 100644
index 0000000000..1a037693bb
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/FairyAPI.scala
@@ -0,0 +1,111 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy
+
+import cats.data.Kleisli
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.FairySpawnRequestErrorOrSpawn
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property._
+
+import java.util.UUID
+
+trait FairyWriteAPI[F[_], G[_], Player] {
+
+ /**
+ * @return 妖精にあげるりんごの開放状態を変更する作用
+ */
+ def updateAppleOpenState(
+ appleConsumeStrategy: FairyAppleConsumeStrategy
+ ): Kleisli[F, Player, Unit]
+
+ /**
+ * @return 妖精を召喚するためのコストを変更する作用
+ */
+ def updateFairySummonCost(fairySummonCost: FairySummonCost): Kleisli[F, Player, Unit]
+
+}
+
+object FairyWriteAPI {
+
+ def apply[F[_], G[_], Player](
+ implicit ev: FairyWriteAPI[F, G, Player]
+ ): FairyWriteAPI[F, G, Player] = ev
+
+}
+
+trait FairyReadAPI[F[_], G[_], Player] {
+
+ /**
+ * @return 妖精にあげるりんごの開放状態を取得する作用
+ */
+ def consumeStrategy(uuid: UUID): F[FairyAppleConsumeStrategy]
+
+ /**
+ * @return 妖精を召喚するためのコストを取得する作用
+ */
+ def fairySummonCost(player: Player): F[FairySummonCost]
+
+ /**
+ * @return 妖精を使っているかを取得する作用
+ */
+ def isFairyAppearing(player: Player): F[Boolean]
+
+ /**
+ * @return 自分の妖精に食べさせたりんごの量の順位を返す作用
+ */
+ def rankByMostConsumedApple(player: Player): F[Option[AppleConsumeAmountRank]]
+
+ /**
+ * @return 妖精に食べさせたりんごの量の上位`top`件を返す作用
+ */
+ def rankingByMostConsumedApple(top: Int): F[Vector[Option[AppleConsumeAmountRank]]]
+
+ /**
+ * @return 妖精が食べたりんごの合計数を返す作用
+ */
+ def totalConsumedApple: F[AppleAmount]
+
+}
+
+object FairyReadAPI {
+
+ def apply[F[_], G[_], Player](
+ implicit ev: FairyReadAPI[F, G, Player]
+ ): FairyReadAPI[F, G, Player] = ev
+
+}
+
+trait FairySpeechAPI[F[_], Player] {
+
+ /**
+ * @return 妖精がいつ帰るのかを送信する作用
+ */
+ def sendDisappearTimeToChat: Kleisli[F, Player, Unit]
+
+}
+
+object FairySpeechAPI {
+
+ def apply[F[_], Player](implicit ev: FairySpeechAPI[F, Player]): FairySpeechAPI[F, Player] =
+ ev
+
+}
+
+trait FairySummonAPI[F[_], Player] {
+
+ /**
+ * 召喚に失敗した場合はエラーを返す
+ * 成功した場合は召喚する作用を返す
+ */
+ def fairySummonRequest: Kleisli[F, Player, FairySpawnRequestErrorOrSpawn[F]]
+}
+
+object FairySummonAPI {
+
+ def apply[F[_], Player](implicit ev: FairySummonAPI[F, Player]): FairySummonAPI[F, Player] =
+ ev
+
+}
+
+trait FairyAPI[F[_], G[_], Player]
+ extends FairyReadAPI[F, G, Player]
+ with FairyWriteAPI[F, G, Player]
+ with FairySpeechAPI[F, Player]
+ with FairySummonAPI[F, Player]
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/System.scala
new file mode 100644
index 0000000000..f02daa6925
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/System.scala
@@ -0,0 +1,135 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy
+
+import cats.data.Kleisli
+import cats.effect.{ConcurrentEffect, IO, SyncIO}
+import com.github.unchama.concurrent.RepeatingTaskContext
+import com.github.unchama.datarepository.bukkit.player.BukkitRepositoryControls
+import com.github.unchama.datarepository.template.RepositoryDefinition
+import com.github.unchama.minecraft.actions.OnMinecraftServerThread
+import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.timer
+import com.github.unchama.seichiassist.meta.subsystem.Subsystem
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
+import com.github.unchama.seichiassist.subsystems.mana.ManaApi
+import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions.SummonFairy
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.repository.FairyManaRecoveryRoutineFiberRepositoryDefinition
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.actions.BukkitSummonFairy
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.listeners.FairyPlayerJoinGreeter
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.routines.BukkitFairyRoutine
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.{
+ BukkitFairySpeech,
+ BukkitFairySummonRequest
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property._
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech.FairySpeech
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.{
+ FairyPersistence,
+ FairySpawnRequestErrorOrSpawn,
+ FairySummonRequest
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.infrastructure.JdbcFairyPersistence
+import org.bukkit.entity.Player
+import org.bukkit.event.Listener
+import org.bukkit.inventory.ItemStack
+
+import java.util.UUID
+
+trait System[F[_], G[_], Player] extends Subsystem[F] {
+ val api: FairyAPI[F, G, Player]
+}
+
+object System {
+
+ def wired(
+ implicit breakCountAPI: BreakCountAPI[IO, SyncIO, Player],
+ voteAPI: VoteAPI[IO, Player],
+ manaApi: ManaApi[IO, SyncIO, Player],
+ mineStackAPI: MineStackAPI[IO, Player, ItemStack],
+ fairySpeechAPI: com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.FairySpeechAPI[
+ IO,
+ Player
+ ],
+ repeatingTaskContext: RepeatingTaskContext,
+ concurrentEffect: ConcurrentEffect[IO],
+ minecraftServerThread: OnMinecraftServerThread[IO]
+ ): SyncIO[System[IO, SyncIO, Player]] = {
+ implicit val persistence: FairyPersistence[IO] = new JdbcFairyPersistence[IO]
+ implicit val fairySpeechProvider: FairySpeech[IO, Player] = new BukkitFairySpeech[IO]
+ val fairyRoutine = new BukkitFairyRoutine(fairySpeechProvider)
+
+ for {
+ fairyRecoveryRoutineFiberRepositoryControls <- BukkitRepositoryControls.createHandles(
+ RepositoryDefinition
+ .Phased
+ .TwoPhased(
+ FairyManaRecoveryRoutineFiberRepositoryDefinition
+ .initialization[Player](fairyRoutine),
+ FairyManaRecoveryRoutineFiberRepositoryDefinition.finalization[Player]
+ )
+ )
+ } yield {
+ new System[IO, SyncIO, Player] {
+ implicit val summonFairy: SummonFairy[IO, Player] =
+ new BukkitSummonFairy[IO, SyncIO]
+ val summonRequest: FairySummonRequest[IO, Player] =
+ new BukkitFairySummonRequest[IO, SyncIO]
+
+ override implicit val api: FairyAPI[IO, SyncIO, Player] =
+ new FairyAPI[IO, SyncIO, Player] {
+ override def consumeStrategy(uuid: UUID): IO[FairyAppleConsumeStrategy] =
+ persistence.appleConsumeStrategy(uuid)
+
+ override def updateAppleOpenState(
+ appleConsumeStrategy: FairyAppleConsumeStrategy
+ ): Kleisli[IO, Player, Unit] = Kleisli { player =>
+ persistence.updateAppleConsumeStrategy(player.getUniqueId, appleConsumeStrategy)
+ }
+
+ override def updateFairySummonCost(
+ fairySummonCost: FairySummonCost
+ ): Kleisli[IO, Player, Unit] = Kleisli { player =>
+ persistence.updateFairySummonCost(player.getUniqueId, fairySummonCost)
+ }
+
+ override def fairySummonCost(player: Player): IO[FairySummonCost] =
+ persistence.fairySummonCost(player.getUniqueId)
+
+ override def isFairyAppearing(player: Player): IO[Boolean] =
+ persistence.isFairyUsing(player.getUniqueId)
+
+ override def rankByMostConsumedApple(
+ player: Player
+ ): IO[Option[AppleConsumeAmountRank]] =
+ persistence.rankByConsumedAppleAmountByFairy(player.getUniqueId)
+
+ override def rankingByMostConsumedApple(
+ top: Int
+ ): IO[Vector[Option[AppleConsumeAmountRank]]] =
+ persistence.fetchMostConsumedApplePlayersByFairy(top)
+
+ override def totalConsumedApple: IO[AppleAmount] =
+ persistence.totalConsumedAppleAmount
+
+ override def sendDisappearTimeToChat: Kleisli[IO, Player, Unit] = Kleisli {
+ player => fairySpeechProvider.speechEndTime(player)
+ }
+
+ override def fairySummonRequest
+ : Kleisli[IO, Player, FairySpawnRequestErrorOrSpawn[IO]] = Kleisli { player =>
+ summonRequest.summonRequest(player)
+ }
+
+ }
+
+ override val managedRepositoryControls: Seq[BukkitRepositoryControls[IO, _]] =
+ Seq(fairyRecoveryRoutineFiberRepositoryControls).map(
+ _.coerceFinalizationContextTo[IO]
+ )
+
+ override val listeners: Seq[Listener] = Seq(new FairyPlayerJoinGreeter)
+ }
+ }
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/FairyRoutine.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/FairyRoutine.scala
new file mode 100644
index 0000000000..da2893a256
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/FairyRoutine.scala
@@ -0,0 +1,10 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions
+
+trait FairyRoutine[F[_], Player] {
+
+ /**
+ * @return 妖精の定期実行プロセスを開始する作用
+ */
+ def start(player: Player): F[Nothing]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/RecoveryMana.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/RecoveryMana.scala
new file mode 100644
index 0000000000..520faebb95
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/RecoveryMana.scala
@@ -0,0 +1,16 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions
+
+trait RecoveryMana[F[_]] {
+
+ /**
+ * @return 妖精がマナを回復する作用
+ */
+ def recovery: F[Unit]
+
+}
+
+object RecoveryMana {
+
+ def apply[F[_]](implicit ev: RecoveryMana[F]): RecoveryMana[F] = ev
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/SummonFairy.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/SummonFairy.scala
new file mode 100644
index 0000000000..784963cd13
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/actions/SummonFairy.scala
@@ -0,0 +1,17 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions
+
+trait SummonFairy[F[_], Player] {
+
+ /**
+ * @return 妖精を召喚する作用
+ */
+ def summon(player: Player): F[Unit]
+
+}
+
+object SummonFairy {
+
+ def apply[F[_], Player](implicit ev: SummonFairy[F, Player]): SummonFairy[F, Player] =
+ ev
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/repository/FairyManaRecoveryRoutineFiberRepositoryDefinition.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/repository/FairyManaRecoveryRoutineFiberRepositoryDefinition.scala
new file mode 100644
index 0000000000..62e38c7a19
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/application/repository/FairyManaRecoveryRoutineFiberRepositoryDefinition.scala
@@ -0,0 +1,40 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.repository
+
+import cats.effect.concurrent.Deferred
+import cats.effect.{ConcurrentEffect, Fiber, IO, SyncIO}
+import com.github.unchama.concurrent.RepeatingTaskContext
+import com.github.unchama.datarepository.template.finalization.RepositoryFinalization
+import com.github.unchama.datarepository.template.initialization.TwoPhasedRepositoryInitialization
+import com.github.unchama.generic.effect.EffectExtra
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions.FairyRoutine
+
+object FairyManaRecoveryRoutineFiberRepositoryDefinition {
+
+ def initialization[Player](fairyRoutine: FairyRoutine[IO, Player])(
+ implicit context: RepeatingTaskContext,
+ concurrentEffect: ConcurrentEffect[IO]
+ ): TwoPhasedRepositoryInitialization[SyncIO, Player, Deferred[IO, Fiber[IO, Nothing]]] =
+ TwoPhasedRepositoryInitialization
+ .withoutPrefetching[SyncIO, Player, Deferred[IO, Fiber[IO, Nothing]]] { player =>
+ for {
+ promise <- Deferred.in[SyncIO, IO, Fiber[IO, Nothing]]
+ _ <-
+ EffectExtra.runAsyncAndForget[IO, SyncIO, Unit] {
+ fairyRoutine
+ .start(player)
+ .start(IO.contextShift(context))
+ .flatMap(fiber => promise.complete(fiber))
+ }
+ } yield promise
+ }
+
+ def finalization[Player]
+ : RepositoryFinalization[SyncIO, Player, Deferred[IO, Fiber[IO, Nothing]]] =
+ RepositoryFinalization
+ .withoutAnyPersistence[SyncIO, Player, Deferred[IO, Fiber[IO, Nothing]]] { (_, promise) =>
+ EffectExtra.runAsyncAndForget[IO, SyncIO, Unit] {
+ promise.get.flatMap(_.cancel)
+ }
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/BukkitFairySpeech.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/BukkitFairySpeech.scala
new file mode 100644
index 0000000000..2b524699a9
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/BukkitFairySpeech.scala
@@ -0,0 +1,90 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit
+
+import cats.effect.Sync
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.FairyPersistence
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property._
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.resources.FairyMessageTable
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech.FairySpeech
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.FairySpeechAPI
+import io.chrisdavenport.cats.effect.time.JavaTime
+import org.bukkit.entity.Player
+
+import java.time.ZoneId
+import scala.util.Random
+
+class BukkitFairySpeech[F[_]: Sync: JavaTime](
+ implicit fairyPersistence: FairyPersistence[F],
+ fairySpeechAPI: FairySpeechAPI[F, Player]
+) extends FairySpeech[F, Player] {
+
+ import cats.implicits._
+
+ private def getSummonMessagesByStartHour(
+ startHour: Int,
+ nameCalledByFairy: ScreenNameForFairy
+ ): FairyMessageChoice = {
+ if (4 <= startHour && startHour < 10)
+ FairyMessageTable.morningMessages(nameCalledByFairy)
+ else if (10 <= startHour && startHour < 18)
+ FairyMessageTable.dayMessages(nameCalledByFairy)
+ else
+ FairyMessageTable.nightMessages(nameCalledByFairy)
+ }
+
+ override def summonSpeech(player: Player): F[Unit] =
+ for {
+ startHour <- JavaTime[F].getLocalDateTime(ZoneId.systemDefault()).map(_.getHour)
+ nameCalledByFairy = ScreenNameForFairy(player.getName)
+ fairyMessages = getSummonMessagesByStartHour(startHour, nameCalledByFairy)
+ message <- randomMessage(fairyMessages)
+ _ <- fairySpeechAPI.speech(player, Seq(message))
+ } yield ()
+
+ override def speechRandomly(
+ player: Player,
+ fairyManaRecoveryState: FairyManaRecoveryState
+ ): F[Unit] = {
+ val nameCalledByFairy = ScreenNameForFairy(player.getName)
+ val messages = fairyManaRecoveryState match {
+ case FairyManaRecoveryState.Full =>
+ FairyMessageTable.manaFullMessages
+ case FairyManaRecoveryState.RecoveredWithApple =>
+ FairyMessageTable.consumed
+ case FairyManaRecoveryState.RecoveredWithoutApple =>
+ FairyMessageTable.notConsumed
+ }
+ for {
+ message <- randomMessage(messages(nameCalledByFairy))
+ _ <- fairySpeechAPI.speech(player, Seq(message))
+ } yield ()
+ }
+
+ override def speechEndTime(player: Player): F[Unit] = {
+ for {
+ endTimeOpt <- fairyPersistence.fairyEndTime(player.getUniqueId)
+ endTime = endTimeOpt.get.endTime
+ _ <- fairySpeechAPI.speech(
+ player,
+ Seq(FairyMessage(s"僕は${endTime.getHour}:${endTime.getMinute}には帰るよー。"))
+ )
+ } yield ()
+ }
+
+ override def welcomeBack(player: Player): F[Unit] = for {
+ _ <- fairySpeechAPI.speech(player, Seq(FairyMessage(s"おかえり!${player.getName}")))
+ } yield ()
+
+ override def bye(player: Player): F[Unit] = for {
+ _ <- fairySpeechAPI.speech(
+ player,
+ Seq(FairyMessage(s"あっ、もうこんな時間だ!"), FairyMessage(s"じゃーねー!${player.getName}"))
+ )
+ } yield ()
+
+ private def randomMessage(fairyMessages: FairyMessageChoice): F[FairyMessage] =
+ Sync[F].delay {
+ val messages = fairyMessages.messages.toVector
+ messages(Random.nextInt(messages.size))
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/BukkitFairySummonRequest.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/BukkitFairySummonRequest.scala
new file mode 100644
index 0000000000..8d91c91211
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/BukkitFairySummonRequest.scala
@@ -0,0 +1,47 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit
+
+import cats.effect.Sync
+import com.github.unchama.generic.ContextCoercion
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions.SummonFairy
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairySummonRequestError
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.{
+ FairyPersistence,
+ FairySpawnRequestErrorOrSpawn,
+ FairySummonRequest
+}
+import org.bukkit.entity.Player
+
+class BukkitFairySummonRequest[F[_]: Sync, G[_]: ContextCoercion[*[_], F]](
+ implicit breakCountAPI: BreakCountAPI[F, G, Player],
+ voteAPI: VoteAPI[F, Player],
+ fairyPersistence: FairyPersistence[F],
+ summonFairy: SummonFairy[F, Player]
+) extends FairySummonRequest[F, Player] {
+
+ import cats.implicits._
+
+ override def summonRequest(player: Player): F[FairySpawnRequestErrorOrSpawn[F]] = {
+ val uuid = player.getUniqueId
+ for {
+ usingState <- fairyPersistence.isFairyUsing(uuid)
+ effectPoints <- voteAPI.effectPoints(player)
+ fairySummonCost <- fairyPersistence.fairySummonCost(uuid)
+ seichiAmountRepository <- ContextCoercion(
+ breakCountAPI.seichiAmountDataRepository(player).read
+ )
+ seichiLevel = seichiAmountRepository.levelCorrespondingToExp.level
+ } yield {
+ if (seichiLevel < 10)
+ Left(FairySummonRequestError.NotEnoughSeichiLevel)
+ else if (usingState)
+ Left(FairySummonRequestError.AlreadyFairySummoned)
+ else if (effectPoints.value < fairySummonCost.value * 2)
+ Left(FairySummonRequestError.NotEnoughEffectPoint)
+ else
+ Right(summonFairy.summon(player))
+ }
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/actions/BukkitRecoveryMana.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/actions/BukkitRecoveryMana.scala
new file mode 100644
index 0000000000..6c552ab691
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/actions/BukkitRecoveryMana.scala
@@ -0,0 +1,203 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.actions
+
+import cats.effect.ConcurrentEffect
+import com.github.unchama.generic.ContextCoercion
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
+import com.github.unchama.seichiassist.subsystems.mana.ManaApi
+import com.github.unchama.seichiassist.subsystems.mana.domain.ManaAmount
+import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions.RecoveryMana
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.FairyPersistence
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.{
+ AppleAmount,
+ FairyAppleConsumeStrategy,
+ FairyManaRecoveryState
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech.FairySpeech
+import com.github.unchama.targetedeffect.SequentialEffect
+import com.github.unchama.targetedeffect.commandsender.MessageEffectF
+import io.chrisdavenport.cats.effect.time.JavaTime
+import org.bukkit.ChatColor._
+import org.bukkit.entity.Player
+import org.bukkit.inventory.ItemStack
+
+import java.time.ZoneId
+import java.util.UUID
+import scala.util.Random
+
+class BukkitRecoveryMana[F[_]: ConcurrentEffect: JavaTime, G[_]: ContextCoercion[*[_], F]](
+ player: Player,
+ fairySpeech: FairySpeech[F, Player]
+)(
+ implicit breakCountAPI: BreakCountAPI[F, G, Player],
+ voteAPI: VoteAPI[F, Player],
+ manaApi: ManaApi[F, G, Player],
+ fairyPersistence: FairyPersistence[F],
+ mineStackAPI: MineStackAPI[F, Player, ItemStack]
+) extends RecoveryMana[F] {
+
+ private val uuid: UUID = player.getUniqueId
+
+ import cats.implicits._
+
+ override def recovery: F[Unit] =
+ for {
+ isFairyUsing <- fairyPersistence.isFairyUsing(uuid)
+ fairyEndTimeOpt <- fairyPersistence.fairyEndTime(uuid)
+ oldManaAmount <- ContextCoercion {
+ manaApi.readManaAmount(player)
+ }
+ _ <- {
+ fairySpeech.speechRandomly(player, FairyManaRecoveryState.Full)
+ }.whenA(isFairyUsing && oldManaAmount.isFull)
+
+ appleConsumptionAmount <- computeAppleConsumptionAmount
+ finallyAppleConsumptionAmount <- computeFinallyAppleConsumptionAmount(
+ appleConsumptionAmount
+ )
+ recoveryManaAmount <- computeManaRecoveryAmount(finallyAppleConsumptionAmount)
+
+ gachaRingoObject <- mineStackAPI.mineStackObjectList.findByName("gachaimo")
+
+ mineStackedGachaRingoAmount <- mineStackAPI
+ .mineStackRepository
+ .getStackedAmountOf(player, gachaRingoObject.get)
+
+ _ <- MessageEffectF(s"$RESET$YELLOW${BOLD}MineStackにがちゃりんごがないようです。。。")
+ .apply(player)
+ .whenA(
+ isFairyUsing && !oldManaAmount.isFull && appleConsumptionAmount > mineStackedGachaRingoAmount
+ )
+
+ _ <- {
+ fairyPersistence.increaseConsumedAppleAmountByFairy(
+ uuid,
+ AppleAmount(finallyAppleConsumptionAmount)
+ ) >>
+ ContextCoercion(
+ manaApi.manaAmount(player).restoreAbsolute(ManaAmount(recoveryManaAmount))
+ ) >>
+ fairySpeech.speechRandomly(
+ player,
+ if (finallyAppleConsumptionAmount == 0)
+ FairyManaRecoveryState.RecoveredWithoutApple
+ else FairyManaRecoveryState.RecoveredWithApple
+ ) >>
+ mineStackAPI
+ .mineStackRepository
+ .subtractStackedAmountOf(
+ player,
+ gachaRingoObject.get,
+ finallyAppleConsumptionAmount
+ ) >>
+ SequentialEffect(
+ MessageEffectF(s"$RESET$YELLOW${BOLD}マナ妖精が${recoveryManaAmount}マナを回復してくれました"),
+ if (finallyAppleConsumptionAmount != 0)
+ MessageEffectF(
+ s"$RESET$YELLOW${BOLD}あっ!${finallyAppleConsumptionAmount}個のがちゃりんごが食べられてる!"
+ )
+ else MessageEffectF(s"$RESET$YELLOW${BOLD}あなたは妖精にりんごを渡しませんでした。")
+ ).apply(player)
+ }.whenA(isFairyUsing && !oldManaAmount.isFull)
+ finishUse <- JavaTime[F]
+ .getLocalDateTime(ZoneId.systemDefault())
+ .map(now => isFairyUsing && fairyEndTimeOpt.exists(_.endTime.isBefore(now)))
+ _ <- {
+ fairySpeech
+ .bye(player) >> fairyPersistence.updateIsFairyUsing(uuid, isFairyUsing = false)
+ }.whenA(finishUse)
+ } yield ()
+
+ /**
+ * MineStackに入っているがちゃりんごの数を考慮していない
+ * がちゃりんごの消費量を計算します。
+ */
+ private def computeAppleConsumptionAmount: F[Int] = for {
+ seichiAmountData <- ContextCoercion(breakCountAPI.seichiAmountDataRepository(player).read)
+ voteStreaks <- voteAPI.currentConsecutiveVoteStreakDays(uuid)
+ appleOpenState <- fairyPersistence.appleConsumeStrategy(uuid)
+ } yield {
+ val playerLevel = seichiAmountData.levelCorrespondingToExp
+
+ val defaultAmount = Math.pow(playerLevel.level / 10, 2)
+
+ val voteStreakDays = voteStreaks.value
+ // 連続投票を適用した除算量
+ val chainVoteDivisor =
+ if (voteStreakDays >= 30) 2
+ else if (voteStreakDays >= 10) 1.5
+ else if (voteStreakDays >= 3) 1.25
+ else 1
+
+ // りんごの開放状況を適用した除算量
+ val appleConsumeStrategyDivisor = appleOpenState match {
+ case FairyAppleConsumeStrategy.Permissible => 1
+ case FairyAppleConsumeStrategy.Consume => 2
+ case FairyAppleConsumeStrategy.LessConsume => 3
+ case FairyAppleConsumeStrategy.NoConsume => 4
+ }
+
+ // りんごの開放状況まで適用したりんごの消費量 (暫定)
+ val appleOpenStateReflectedAmount =
+ (defaultAmount / chainVoteDivisor).toInt / appleConsumeStrategyDivisor
+
+ // 妖精がつまみ食いする量
+ val amountEatenByKnob =
+ if (appleOpenStateReflectedAmount >= 10)
+ new Random().nextInt(appleOpenStateReflectedAmount / 10)
+ else 0
+
+ /*
+ 最終的に算出されたりんごの消費量
+ (現時点では持っているりんごの数を
+ 考慮していないので消費量は確定していない)
+ */
+ Math.max(appleOpenStateReflectedAmount + amountEatenByKnob, 1)
+ }
+
+ /**
+ * MineStackに入っているがちゃりんごの数を考慮した
+ * がちゃりんごの消費量を計算します。
+ */
+ private def computeFinallyAppleConsumptionAmount(appleConsumptionAmount: Int): F[Int] = for {
+ appleOpenState <- fairyPersistence.appleConsumeStrategy(uuid)
+ gachaRingoObject <- mineStackAPI.mineStackObjectList.findByName("gachaimo")
+ mineStackedGachaRingoAmount <- mineStackAPI
+ .mineStackRepository
+ .getStackedAmountOf(player, gachaRingoObject.get)
+ } yield {
+ // りんごの消費量
+ if (appleOpenState == FairyAppleConsumeStrategy.NoConsume)
+ 0
+ else if (mineStackedGachaRingoAmount > appleConsumptionAmount)
+ appleConsumptionAmount
+ else mineStackedGachaRingoAmount.toInt
+ }
+
+ /**
+ * マナの回復量を計算します。
+ */
+ private def computeManaRecoveryAmount(appleConsumptionAmount: Int): F[Int] = for {
+ defaultRecoveryManaAmount <- fairyPersistence.fairyRecoveryMana(uuid)
+ appleOpenState <- fairyPersistence.appleConsumeStrategy(uuid)
+ } yield {
+ val appleOpenStateDivision =
+ if (appleOpenState == FairyAppleConsumeStrategy.NoConsume || appleConsumptionAmount == 0)
+ 4
+ else if (appleOpenState == FairyAppleConsumeStrategy.LessConsume) 3
+ else if (appleOpenState == FairyAppleConsumeStrategy.Consume) 2
+ else 1
+
+ val reflectedAppleOpenStateAmount =
+ defaultRecoveryManaAmount.recoveryMana / appleOpenStateDivision
+
+ val randomizedAdd =
+ if (reflectedAppleOpenStateAmount >= 50)
+ Random.nextInt(reflectedAppleOpenStateAmount / 50)
+ else 0
+
+ reflectedAppleOpenStateAmount + randomizedAdd
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/actions/BukkitSummonFairy.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/actions/BukkitSummonFairy.scala
new file mode 100644
index 0000000000..24ae552371
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/actions/BukkitSummonFairy.scala
@@ -0,0 +1,50 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.actions
+
+import cats.effect.Sync
+import com.github.unchama.generic.ContextCoercion
+import com.github.unchama.seichiassist.subsystems.mana.ManaApi
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.domain.EffectPoint
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions.SummonFairy
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.FairyPersistence
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyRecoveryManaAmount
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech.FairySpeech
+import com.github.unchama.targetedeffect.commandsender.MessageEffectF
+import org.bukkit.ChatColor._
+import org.bukkit.entity.Player
+
+class BukkitSummonFairy[F[_]: Sync, G[_]: ContextCoercion[*[_], F]](
+ implicit voteAPI: VoteAPI[F, Player],
+ manaApi: ManaApi[F, G, Player],
+ fairyPersistence: FairyPersistence[F],
+ fairySpeech: FairySpeech[F, Player]
+) extends SummonFairy[F, Player] {
+
+ import cats.implicits._
+
+ override def summon(player: Player): F[Unit] = {
+ val uuid = player.getUniqueId
+ for {
+ _ <- fairyPersistence.updateIsFairyUsing(uuid, isFairyUsing = true)
+ manaAmount <- ContextCoercion {
+ manaApi.readManaAmount(player)
+ }
+ levelCappedManaAmount = manaAmount.cap.value
+ recoveryManaAmount = FairyRecoveryManaAmount.manaAmountAt(levelCappedManaAmount)
+ uuid = player.getUniqueId
+ fairySummonCost <- fairyPersistence.fairySummonCost(uuid)
+ _ <- voteAPI.decreaseEffectPoint(uuid, EffectPoint(fairySummonCost.value * 2))
+ _ <- fairyPersistence.updateFairyRecoveryMana(uuid, recoveryManaAmount)
+ _ <- fairyPersistence.updateFairyEndTime(uuid, fairySummonCost.endTime)
+ _ <- fairySpeech.summonSpeech(player)
+ _ <- MessageEffectF(
+ List(
+ s"$RESET$YELLOW${BOLD}妖精を呼び出しました!",
+ s"$RESET$YELLOW${BOLD}この子は1分間に約${recoveryManaAmount.recoveryMana}マナ",
+ s"$RESET$YELLOW${BOLD}回復させる力を持っているようです。"
+ )
+ ).apply(player)
+ } yield ()
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/listeners/FairyPlayerJoinGreeter.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/listeners/FairyPlayerJoinGreeter.scala
new file mode 100644
index 0000000000..ca64614baa
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/listeners/FairyPlayerJoinGreeter.scala
@@ -0,0 +1,45 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.listeners
+
+import cats.effect.{ConcurrentEffect, IO}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.FairyPersistence
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech.FairySpeech
+import org.bukkit.ChatColor._
+import org.bukkit.entity.Player
+import org.bukkit.event.player.{AsyncPlayerPreLoginEvent, PlayerJoinEvent}
+import org.bukkit.event.{EventHandler, EventPriority, Listener}
+
+import java.time.LocalDateTime
+
+class FairyPlayerJoinGreeter(
+ implicit fairyPersistence: FairyPersistence[IO],
+ fairySpeech: FairySpeech[IO, Player],
+ concurrentEffect: ConcurrentEffect[IO]
+) extends Listener {
+
+ import cats.implicits._
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ def onAsyncPlayerPreLogin(e: AsyncPlayerPreLoginEvent): Unit = {
+ fairyPersistence.initializePlayerData(e.getUniqueId).unsafeRunAsyncAndForget()
+ }
+
+ @EventHandler
+ def onJoin(e: PlayerJoinEvent): Unit = {
+ val player = e.getPlayer
+ val uuid = player.getUniqueId
+ val program = for {
+ isUsing <- fairyPersistence.isFairyUsing(uuid)
+ endTime <- fairyPersistence.fairyEndTime(uuid)
+ isEnd <- IO(LocalDateTime.now()).map(now => endTime.exists(_.endTime.isBefore(now)))
+ _ <- {
+ fairyPersistence.updateIsFairyUsing(uuid, isFairyUsing = false) >> IO(
+ player.sendMessage(s"$LIGHT_PURPLE${BOLD}妖精は何処かへ行ってしまったようだ...")
+ )
+ // 終了時間が今よりも過去だったとき(つまり有効時間終了済み)
+ }.whenA(isUsing && isEnd)
+ _ <- fairySpeech.welcomeBack(player).whenA(isUsing && !isEnd)
+ } yield ()
+ program.unsafeRunSync()
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/routines/BukkitFairyRoutine.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/routines/BukkitFairyRoutine.scala
new file mode 100644
index 0000000000..95507e5cc9
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/bukkit/routines/BukkitFairyRoutine.scala
@@ -0,0 +1,47 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.routines
+
+import cats.effect.{ConcurrentEffect, IO, SyncIO, Timer}
+import com.github.unchama.concurrent.{RepeatingRoutine, RepeatingTaskContext}
+import com.github.unchama.minecraft.actions.OnMinecraftServerThread
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
+import com.github.unchama.seichiassist.subsystems.mana.ManaApi
+import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
+import com.github.unchama.seichiassist.subsystems.vote.VoteAPI
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.application.actions.FairyRoutine
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.bukkit.actions.BukkitRecoveryMana
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.FairyPersistence
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech.FairySpeech
+import org.bukkit.entity.Player
+import org.bukkit.inventory.ItemStack
+
+import scala.concurrent.duration.FiniteDuration
+
+class BukkitFairyRoutine(fairySpeech: FairySpeech[IO, Player])(
+ implicit breakCountAPI: BreakCountAPI[IO, SyncIO, Player],
+ voteAPI: VoteAPI[IO, Player],
+ manaApi: ManaApi[IO, SyncIO, Player],
+ context: RepeatingTaskContext,
+ fairyPersistence: FairyPersistence[IO],
+ concurrentEffect: ConcurrentEffect[IO],
+ minecraftServerThread: OnMinecraftServerThread[IO],
+ mineStackAPI: MineStackAPI[IO, Player, ItemStack]
+) extends FairyRoutine[IO, Player] {
+
+ override def start(player: Player): IO[Nothing] = {
+
+ val repeatInterval: IO[FiniteDuration] = IO {
+ import scala.concurrent.duration._
+
+ 1.minute
+ }
+
+ implicit val timer: Timer[IO] = IO.timer(context)
+
+ RepeatingRoutine.permanentRoutine(
+ repeatInterval,
+ minecraftServerThread.runAction {
+ new BukkitRecoveryMana[IO, SyncIO](player, fairySpeech).recovery.runAsync(_ => IO.unit)
+ }
+ )
+ }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/FairyPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/FairyPersistence.scala
new file mode 100644
index 0000000000..9331e34689
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/FairyPersistence.scala
@@ -0,0 +1,111 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain
+
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.{
+ AppleAmount,
+ AppleConsumeAmountRank,
+ FairyAppleConsumeStrategy,
+ FairyEndTime,
+ FairyRecoveryMana,
+ FairySummonCost
+}
+
+import java.util.UUID
+
+trait FairyPersistence[F[_]] {
+
+ /**
+ * プレイヤーデータを作成する。このメソッドの実装が返す作用は、冪等でなければならない。
+ * @return プレイヤーデータを作成するべき等な作用
+ */
+ def initializePlayerData(player: UUID): F[Unit]
+
+ /**
+ * @return 妖精に開放するりんごの状態を変更する作用
+ */
+ def updateAppleConsumeStrategy(player: UUID, openState: FairyAppleConsumeStrategy): F[Unit]
+
+ /**
+ * @return 妖精に開放するりんごの状態を取得する作用
+ */
+ def appleConsumeStrategy(player: UUID): F[FairyAppleConsumeStrategy]
+
+ /**
+ * @return 妖精が召喚するためのコストを変更する作用
+ */
+ def updateFairySummonCost(player: UUID, fairySummonCost: FairySummonCost): F[Unit]
+
+ /**
+ * @return 妖精を召喚するためのコストを取得する作用
+ */
+ def fairySummonCost(player: UUID): F[FairySummonCost]
+
+ /**
+ * @return 妖精が召喚されているかを更新する作用
+ */
+ def updateIsFairyUsing(player: UUID, isFairyUsing: Boolean): F[Unit]
+
+ /**
+ * @return 妖精が召喚されているかを取得する作用
+ */
+ def isFairyUsing(player: UUID): F[Boolean]
+
+ /**
+ * @return 妖精が回復するマナの量を変更する作用
+ */
+ def updateFairyRecoveryMana(player: UUID, fairyRecoveryMana: FairyRecoveryMana): F[Unit]
+
+ /**
+ * @return 妖精が回復するマナの量を取得する作用
+ */
+ def fairyRecoveryMana(player: UUID): F[FairyRecoveryMana]
+
+ /**
+ * @return 妖精の効果が終了する時刻を変更する作用
+ */
+ def updateFairyEndTime(player: UUID, fairyEndTime: FairyEndTime): F[Unit]
+
+ /**
+ * @return 妖精の効果が終了する時刻を取得する作用
+ */
+ def fairyEndTime(player: UUID): F[Option[FairyEndTime]]
+
+ /**
+ * @return 妖精が指定プレイヤーから食べたりんごの量を増加させる作用
+ */
+ def increaseConsumedAppleAmountByFairy(player: UUID, appleAmount: AppleAmount): F[Unit]
+
+ /**
+ * @return 妖精が食べたりんごの量を取得する作用
+ */
+ def consumedAppleAmountByFairy(player: UUID): F[Option[AppleAmount]]
+
+ /**
+ * @return 自分の妖精に食べさせたりんごの量の順位を返す作用
+ */
+ def rankByConsumedAppleAmountByFairy(player: UUID): F[Option[AppleConsumeAmountRank]]
+
+ /**
+ * 妖精に食べさせたりんごの量が多いプレイヤーを上位とし、そのランキングの上から指定した件数を返す
+ * ただし、要素数が`top`件あることは保証しない。
+ *
+ * @param top 最上位から何番目まで取得するか件数を指定する。0以下であってはならない。
+ * @return 指定した件数が要素数となり、その並びが消費量の降順になっているような順序つきのコレクションを返す作用。
+ */
+ def fetchMostConsumedApplePlayersByFairy(top: Int): F[Vector[Option[AppleConsumeAmountRank]]]
+
+ /**
+ * @return 妖精が今まで食べたりんごの合計数を返す作用
+ */
+ def totalConsumedAppleAmount: F[AppleAmount]
+
+ /**
+ * @return 妖精が喋るときに音をだすかをトグルする作用
+ */
+ def setPlaySoundOnSpeech(player: UUID, playOnSpeech: Boolean): F[Unit]
+
+ /**
+ * @return 妖精が喋ったときに音を再生するか取得する作用
+ */
+ def playSoundOnFairySpeech(player: UUID): F[Boolean]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/FairySummonRequest.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/FairySummonRequest.scala
new file mode 100644
index 0000000000..cf044fd6e3
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/FairySummonRequest.scala
@@ -0,0 +1,11 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain
+
+trait FairySummonRequest[F[_], Player] {
+
+ /**
+ * @return 妖精の召喚をリクエストする作用
+ * 失敗した場合は失敗理由
+ */
+ def summonRequest(player: Player): F[FairySpawnRequestErrorOrSpawn[F]]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/package.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/package.scala
new file mode 100644
index 0000000000..0720e763aa
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/package.scala
@@ -0,0 +1,9 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy
+
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairySummonRequestError
+
+package object domain {
+
+ type FairySpawnRequestErrorOrSpawn[F[_]] = Either[FairySummonRequestError, F[Unit]]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/AppleAmount.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/AppleAmount.scala
new file mode 100644
index 0000000000..e584b4bc27
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/AppleAmount.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+case class AppleAmount(amount: Int) {
+ require(amount >= 0, "amountは非負の値で指定してください。")
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/AppleConsumeAmountRank.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/AppleConsumeAmountRank.scala
new file mode 100644
index 0000000000..30c0c132a8
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/AppleConsumeAmountRank.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+case class AppleConsumeAmountRank(playerName: String, rank: Int, consumed: AppleAmount) {
+ require(rank >= 1, "rankは1以上の値で指定してください。")
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyAppleConsumeStrategy.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyAppleConsumeStrategy.scala
new file mode 100644
index 0000000000..5b158df7ca
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyAppleConsumeStrategy.scala
@@ -0,0 +1,36 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+import enumeratum._
+
+/**
+ * がちゃりんごの消費戦略を管理するクラス
+ * @param serializedValue
+ * 消費戦略を永続化する際に必要な番号であり、その値は戦略ごとに異なっていなければならない。
+ */
+sealed class FairyAppleConsumeStrategy(val serializedValue: Int) extends EnumEntry
+
+case object FairyAppleConsumeStrategy extends Enum[FairyAppleConsumeStrategy] {
+
+ override val values: IndexedSeq[FairyAppleConsumeStrategy] = findValues
+
+ /**
+ * ガンガン食べるぞ
+ */
+ case object Permissible extends FairyAppleConsumeStrategy(1)
+
+ /**
+ * バッチリたべよう
+ */
+ case object Consume extends FairyAppleConsumeStrategy(2)
+
+ /**
+ * リンゴだいじに
+ */
+ case object LessConsume extends FairyAppleConsumeStrategy(3)
+
+ /**
+ * リンゴつかうな
+ */
+ case object NoConsume extends FairyAppleConsumeStrategy(4)
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyEndTime.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyEndTime.scala
new file mode 100644
index 0000000000..82ec271312
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyEndTime.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+import java.time.LocalDateTime
+
+case class FairyEndTime(endTime: LocalDateTime)
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyLore.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyLore.scala
new file mode 100644
index 0000000000..b0d5b0846c
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyLore.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+import cats.data.NonEmptyList
+
+case class FairyLore(lore: NonEmptyList[String])
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyManaRecoveryState.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyManaRecoveryState.scala
new file mode 100644
index 0000000000..2461d27c89
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyManaRecoveryState.scala
@@ -0,0 +1,25 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+sealed trait FairyManaRecoveryState
+
+/**
+ * 妖精がマナを回復する際に考えられるステータスを定義したオブジェクト
+ */
+object FairyManaRecoveryState {
+
+ /**
+ * マナが完全に回復している
+ */
+ case object Full extends FairyManaRecoveryState
+
+ /**
+ * マナを回復したがりんごを消費しなかった
+ */
+ case object RecoveredWithoutApple extends FairyManaRecoveryState
+
+ /**
+ * りんごを消費してマナを回復した
+ */
+ case object RecoveredWithApple extends FairyManaRecoveryState
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyMessage.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyMessage.scala
new file mode 100644
index 0000000000..b40479eb77
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyMessage.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+case class FairyMessage(message: String) {
+ require(message.nonEmpty)
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyMessageChoice.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyMessageChoice.scala
new file mode 100644
index 0000000000..dd375f5caf
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyMessageChoice.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+import cats.data.NonEmptyVector
+
+case class FairyMessageChoice(messages: NonEmptyVector[FairyMessage])
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyRecoveryMana.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyRecoveryMana.scala
new file mode 100644
index 0000000000..8e14117d35
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyRecoveryMana.scala
@@ -0,0 +1,5 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+case class FairyRecoveryMana(recoveryMana: Int) {
+ require(recoveryMana >= 0)
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyRecoveryManaAmount.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyRecoveryManaAmount.scala
new file mode 100644
index 0000000000..845a70de4c
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairyRecoveryManaAmount.scala
@@ -0,0 +1,15 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+import scala.util.Random
+
+object FairyRecoveryManaAmount {
+
+ def manaAmountAt(levelCappedManaAmount: Double): FairyRecoveryMana = {
+ require(levelCappedManaAmount >= 0.0, "levelCappedManaAmountは非負の値で指定してください。")
+ FairyRecoveryMana(
+ (levelCappedManaAmount / 10 - levelCappedManaAmount / 30 + new Random()
+ .nextInt((levelCappedManaAmount / 20).toInt) / 2.9).toInt + 200
+ )
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairySummonCost.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairySummonCost.scala
new file mode 100644
index 0000000000..42fef30db2
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairySummonCost.scala
@@ -0,0 +1,27 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+import java.time.LocalDateTime
+
+case class FairySummonCost(value: Int) {
+ require(1 <= value && value <= 4, "FairySummonCostは1~4の間で指定してください。")
+
+ /**
+ * @return 妖精の効果が終了する時間
+ * NOTE: 妖精の効果が終了する時間はコストによって定められる。
+ */
+ def endTime: FairyEndTime = {
+ val now = LocalDateTime.now()
+ val validTime = value match {
+ case 1 =>
+ FairyEndTime(now.plusMinutes(30))
+ case 2 =>
+ FairyEndTime(now.plusHours(1))
+ case 3 =>
+ FairyEndTime(now.plusHours(1).plusMinutes(30))
+ case 4 =>
+ FairyEndTime(now.plusHours(2))
+ }
+ validTime
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairySummonRequestError.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairySummonRequestError.scala
new file mode 100644
index 0000000000..692e9e5180
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/FairySummonRequestError.scala
@@ -0,0 +1,25 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+sealed trait FairySummonRequestError
+
+/**
+ * 妖精の召喚をする際に発生する可能性があるエラーを列挙する
+ */
+object FairySummonRequestError {
+
+ /**
+ * 整地レベルが足りなかった
+ */
+ case object NotEnoughSeichiLevel extends FairySummonRequestError
+
+ /**
+ * 妖精がすでに召喚されている
+ */
+ case object AlreadyFairySummoned extends FairySummonRequestError
+
+ /**
+ * 妖精の召喚ポイントが足りなかった
+ */
+ case object NotEnoughEffectPoint extends FairySummonRequestError
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/ScreenNameForFairy.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/ScreenNameForFairy.scala
new file mode 100644
index 0000000000..fcfeb7a494
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/property/ScreenNameForFairy.scala
@@ -0,0 +1,3 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property
+
+case class ScreenNameForFairy(name: String)
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/resources/FairyMessageTable.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/resources/FairyMessageTable.scala
new file mode 100644
index 0000000000..484c3619cc
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/resources/FairyMessageTable.scala
@@ -0,0 +1,95 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.resources
+
+import cats.data.NonEmptyVector
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.{
+ FairyMessage,
+ FairyMessageChoice,
+ ScreenNameForFairy
+}
+
+object FairyMessageTable {
+
+ /**
+ * 朝に妖精を召喚したときに表示されるメッセージ
+ */
+ val morningMessages: ScreenNameForFairy => FairyMessageChoice = (name: ScreenNameForFairy) =>
+ FairyMessageChoice(
+ NonEmptyVector.of(
+ FairyMessage(s"おはよ!${name.name}"),
+ FairyMessage(s"ヤッホー${name.name}"),
+ FairyMessage(s"ふわぁ。。。${name.name}朝は早いね。"),
+ FairyMessage("うーん、今日も一日頑張ろ!"),
+ FairyMessage(s"今日は整地日和だね!${name.name}")
+ )
+ )
+
+ /**
+ * 昼に妖精を召喚したときに表示されるメッセージ
+ */
+ val dayMessages: ScreenNameForFairy => FairyMessageChoice = (name: ScreenNameForFairy) =>
+ FairyMessageChoice(
+ NonEmptyVector.of(
+ FairyMessage(s"やあ!${name.name}"),
+ FairyMessage(s"ヤッホー${name.name}!"),
+ FairyMessage(s"あっ、${name.name}じゃん。丁度お腹空いてたんだ!"),
+ FairyMessage(s"この匂い…${name.name}ってがちゃりんごいっぱい持ってる…?"),
+ FairyMessage("今日のおやつはがちゃりんごいっぱいだ!")
+ )
+ )
+
+ // 夜に妖精を召喚したときに表示されるメッセージ
+ val nightMessages: ScreenNameForFairy => FairyMessageChoice = (name: ScreenNameForFairy) =>
+ FairyMessageChoice(
+ NonEmptyVector.of(
+ FairyMessage(s"やあ!${name.name}"),
+ FairyMessage(s"ヤッホー${name.name}!"),
+ FairyMessage(s"ふわぁ。。。${name.name}は夜も元気だね。"),
+ FairyMessage(s"もう寝ようと思ってたのにー。${name.name}はしょうがないなぁ"),
+ FairyMessage("こんな時間に呼ぶなんて…りんごははずんでもらうよ?")
+ )
+ )
+
+ /**
+ * マナが満タンだったときに表示されるメッセージ
+ */
+ val manaFullMessages: ScreenNameForFairy => FairyMessageChoice = (name: ScreenNameForFairy) =>
+ FairyMessageChoice(
+ NonEmptyVector.of(
+ FairyMessage("整地しないのー?"),
+ FairyMessage("たくさん働いて、たくさんりんごを食べようね!"),
+ FairyMessage("僕はいつか大きながちゃりんごを食べ尽して見せるっ!"),
+ FairyMessage("ちょっと食べ疲れちゃった"),
+ FairyMessage(s"${name.name}はどのりんごが好き?僕はがちゃりんご!"),
+ FairyMessage("動いてお腹を空かしていっぱい食べるぞー!")
+ )
+ )
+
+ /**
+ * 妖精にりんごが消費されたときに表示されるメッセージ
+ */
+ val consumed: ScreenNameForFairy => FairyMessageChoice = (name: ScreenNameForFairy) =>
+ FairyMessageChoice(
+ NonEmptyVector.of(
+ FairyMessage("(´~`)モグモグ…"),
+ FairyMessage("がちゃりんごって美味しいよね!"),
+ FairyMessage("あぁ!幸せ!"),
+ FairyMessage(s"${name.name}のりんごはおいしいなぁ"),
+ FairyMessage("いつもりんごをありがとう!")
+ )
+ )
+
+ /**
+ * 妖精がりんごを消費しなかったときに表示されるメッセージ
+ */
+ val notConsumed: ScreenNameForFairy => FairyMessageChoice = (name: ScreenNameForFairy) =>
+ FairyMessageChoice(
+ NonEmptyVector.of(
+ FairyMessage("お腹空いたなぁー。"),
+ FairyMessage("がちゃりんごがっ!食べたいっ!"),
+ FairyMessage("(´;ω;`)ウゥゥ ヒモジイ..."),
+ FairyMessage(s"@うんちゃま ${name.name}が意地悪するんだっ!"),
+ FairyMessage("うわーん!お腹空いたよー!")
+ )
+ )
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/speech/FairySpeech.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/speech/FairySpeech.scala
new file mode 100644
index 0000000000..73df3b1cff
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/domain/speech/FairySpeech.scala
@@ -0,0 +1,33 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.speech
+
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyManaRecoveryState
+
+trait FairySpeech[F[_], Player] {
+
+ /**
+ * @return 妖精が召喚された際のメッセージを[[Player]]に送信する作用
+ */
+ def summonSpeech(player: Player): F[Unit]
+
+ /**
+ * @return [[FairyManaRecoveryState]]に応じた
+ * 妖精のメッセージをランダムに[[Player]]へ送信する作用
+ */
+ def speechRandomly(player: Player, fairyManaRecoveryState: FairyManaRecoveryState): F[Unit]
+
+ /**
+ * @return 妖精がいつ帰るのかを[[Player]]へ知らせる作用
+ */
+ def speechEndTime(player: Player): F[Unit]
+
+ /**
+ * @return 妖精召喚中に[[Player]]が再ログインした際のメッセージを[[Player]]に送信する作用
+ */
+ def welcomeBack(player: Player): F[Unit]
+
+ /**
+ * @return 妖精が帰るときのメッセージを[[Player]]に送信する作用
+ */
+ def bye(player: Player): F[Unit]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/infrastructure/JdbcFairyPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/infrastructure/JdbcFairyPersistence.scala
new file mode 100644
index 0000000000..1e86fc4584
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairy/infrastructure/JdbcFairyPersistence.scala
@@ -0,0 +1,236 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.infrastructure
+
+import cats.effect.Sync
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain._
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property._
+import scalikejdbc.{DB, scalikejdbcSQLInterpolationImplicitDef}
+
+import java.util.UUID
+
+class JdbcFairyPersistence[F[_]: Sync] extends FairyPersistence[F] {
+
+ override def initializePlayerData(player: UUID): F[Unit] = Sync[F].delay {
+ val playerDataCount = DB.readOnly { implicit session =>
+ sql"SELECT COUNT(*) as c FROM vote_fairy where uuid = ${player.toString}"
+ .map(_.int("c"))
+ .single()
+ .apply()
+ .getOrElse(0)
+ }
+
+ if (playerDataCount == 0) {
+ DB.localTx { implicit session =>
+ sql"INSERT INTO vote_fairy (uuid) VALUES (${player.toString})".execute().apply()
+ }
+ }
+ }
+
+ override def updateAppleConsumeStrategy(
+ player: UUID,
+ openState: FairyAppleConsumeStrategy
+ ): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET apple_open_state = ${openState.serializedValue} WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def appleConsumeStrategy(player: UUID): F[FairyAppleConsumeStrategy] =
+ Sync[F].delay {
+ val serializedValue = DB.readOnly { implicit session =>
+ sql"SELECT apple_open_state FROM vote_fairy WHERE uuid = ${player.toString}"
+ .map(_.int("apple_open_state"))
+ .single()
+ .apply()
+ .get
+ }
+ FairyAppleConsumeStrategy.values.find(_.serializedValue == serializedValue).get
+ }
+
+ override def updateFairySummonCost(player: UUID, fairySummonCost: FairySummonCost): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET fairy_summon_cost = ${fairySummonCost.value} WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def fairySummonCost(player: UUID): F[FairySummonCost] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val fairySummonCost =
+ sql"SELECT fairy_summon_cost FROM vote_fairy WHERE uuid = ${player.toString}"
+ .map(_.int("fairy_summon_cost"))
+ .single()
+ .apply()
+ .get
+ FairySummonCost(fairySummonCost)
+ }
+ }
+
+ override def updateIsFairyUsing(player: UUID, isFairyUsing: Boolean): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"""UPDATE vote_fairy
+ | SET is_fairy_using = $isFairyUsing WHERE uuid = ${player.toString}"""
+ .stripMargin
+ .execute()
+ .apply()
+ }
+ }
+
+ override def isFairyUsing(player: UUID): F[Boolean] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ sql"SELECT is_fairy_using FROM vote_fairy WHERE uuid = ${player.toString}"
+ .map(_.boolean("is_fairy_using"))
+ .single()
+ .apply()
+ }.get
+ }
+
+ override def updateFairyRecoveryMana(
+ player: UUID,
+ fairyRecoveryMana: FairyRecoveryMana
+ ): F[Unit] = Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET fairy_recovery_mana_value = ${fairyRecoveryMana.recoveryMana} WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def fairyRecoveryMana(player: UUID): F[FairyRecoveryMana] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val recoveryMana =
+ sql"SELECT fairy_recovery_mana_value FROM vote_fairy WHERE uuid = ${player.toString}"
+ .map(_.int("fairy_recovery_mana_value"))
+ .single()
+ .apply()
+ .get
+ FairyRecoveryMana(recoveryMana)
+ }
+ }
+
+ override def updateFairyEndTime(player: UUID, fairyEndTime: FairyEndTime): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET fairy_end_time = ${fairyEndTime.endTime} WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def fairyEndTime(player: UUID): F[Option[FairyEndTime]] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val dateOpt = sql"SELECT fairy_end_time FROM vote_fairy WHERE uuid = ${player.toString}"
+ .map(_.localDateTime("fairy_end_time"))
+ .single()
+ .apply()
+ dateOpt.map(FairyEndTime)
+ }
+ }
+
+ override def increaseConsumedAppleAmountByFairy(
+ player: UUID,
+ appleAmount: AppleAmount
+ ): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET given_apple_amount = given_apple_amount + ${appleAmount.amount} WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def consumedAppleAmountByFairy(player: UUID): F[Option[AppleAmount]] =
+ Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val appleAmountOpt =
+ sql"SELECT given_apple_amount FROM vote_fairy WHERE uuid = ${player.toString}"
+ .map(_.int("given_apple_amount"))
+ .single()
+ .apply()
+ appleAmountOpt.map(AppleAmount)
+ }
+ }
+
+ override def rankByConsumedAppleAmountByFairy(
+ player: UUID
+ ): F[Option[AppleConsumeAmountRank]] =
+ Sync[F].delay {
+ DB.readOnly { implicit session =>
+ sql"""SELECT vote_fairy.uuid AS uuid,name,given_apple_amount,COUNT(*) AS rank
+ | FROM vote_fairy
+ | INNER JOIN playerdata
+ | ON (playerdata.uuid = vote_fairy.uuid)
+ | ORDER BY rank DESC;"""
+ .stripMargin
+ .map(rs =>
+ rs.string("uuid") -> AppleConsumeAmountRank(
+ rs.string("name"),
+ rs.int("rank"),
+ AppleAmount(rs.int("given_apple_amount"))
+ )
+ )
+ .toList()
+ .apply()
+ .find(_._1 == player.toString)
+ .map(_._2)
+ }
+ }
+
+ override def fetchMostConsumedApplePlayersByFairy(
+ top: Int
+ ): F[Vector[Option[AppleConsumeAmountRank]]] =
+ Sync[F].delay {
+ DB.readOnly { implicit session =>
+ sql"""SELECT name,given_apple_amount,COUNT(*) AS rank FROM vote_fairy
+ | INNER JOIN playerdata ON (vote_fairy.uuid = playerdata.uuid)
+ | ORDER BY rank DESC LIMIT $top;"""
+ .stripMargin
+ .map { rs =>
+ for {
+ name <- rs.stringOpt("name")
+ rank <- rs.intOpt("rank")
+ givenAppleAmount <- rs.intOpt("given_apple_amount")
+ } yield AppleConsumeAmountRank(name, rank, AppleAmount(givenAppleAmount))
+ }
+ .toList()
+ .apply()
+ .toVector
+ }
+ }
+
+ override def totalConsumedAppleAmount: F[AppleAmount] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ val amount = sql"SELECT SUM(given_apple_amount) AS allAppleAmount FROM vote_fairy;"
+ .map(_.int("allAppleAmount"))
+ .single()
+ .apply()
+ .get
+ AppleAmount(amount)
+ }
+ }
+
+ override def setPlaySoundOnSpeech(player: UUID, playOnSpeech: Boolean): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET is_play_fairy_speech_sound = $playOnSpeech WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def playSoundOnFairySpeech(player: UUID): F[Boolean] =
+ Sync[F].delay {
+ DB.readOnly { implicit session =>
+ sql"SELECT is_play_fairy_speech_sound FROM vote_fairy WHERE uuid=${player.toString}"
+ .map(_.boolean("is_play_fairy_speech_sound"))
+ .single()
+ .apply()
+ .get
+ }
+ }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/FairySpeechAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/FairySpeechAPI.scala
new file mode 100644
index 0000000000..f581572861
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/FairySpeechAPI.scala
@@ -0,0 +1,33 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech
+
+import cats.data.Kleisli
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyMessage
+
+import java.util.UUID
+import scala.collection.immutable.Seq
+
+trait FairySpeechAPI[F[_], Player] {
+
+ /**
+ * @return `player`に対して`messages`を妖精が喋る作用
+ */
+ def speech(player: Player, messages: Seq[FairyMessage]): F[Unit]
+
+ /**
+ * @return `player`に対して妖精が喋った際に音を再生するかをトグルする作用
+ */
+ def togglePlaySoundOnSpeech: Kleisli[F, Player, Unit]
+
+ /**
+ * @return `player`に対して妖精が喋った際に音を再生するかどうかを取得する作用
+ */
+ def playSoundOnSpeech(player: UUID): F[Boolean]
+
+}
+
+object FairySpeechAPI {
+
+ def apply[F[_], Player](implicit ev: FairySpeechAPI[F, Player]): FairySpeechAPI[F, Player] =
+ ev
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/System.scala
new file mode 100644
index 0000000000..df672e185d
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/System.scala
@@ -0,0 +1,56 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech
+
+import cats.data.Kleisli
+import cats.effect.{Sync, Timer}
+import com.github.unchama.seichiassist.meta.subsystem.Subsystem
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyMessage
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.bukkit.BukkitFairySpeechGateway
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.domain.{
+ FairySpeechGateway,
+ FairySpeechPersistence
+}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.infrastructure.JdbcFairySpeechPersistence
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.service.FairySpeechService
+import org.bukkit.entity.Player
+
+import java.util.UUID
+
+trait System[F[_], Player] extends Subsystem[F] {
+
+ val api: FairySpeechAPI[F, Player]
+
+}
+
+object System {
+
+ import cats.implicits._
+
+ def wired[F[_]: Sync: Timer]: System[F, Player] = {
+ val speechGateway: Player => FairySpeechGateway[F] = player =>
+ new BukkitFairySpeechGateway[F](player)
+ val speechService: Player => FairySpeechService[F] = player =>
+ new FairySpeechService[F](speechGateway(player))
+ val persistence: FairySpeechPersistence[F] = new JdbcFairySpeechPersistence[F]
+
+ new System[F, Player] {
+ override val api: FairySpeechAPI[F, Player] = new FairySpeechAPI[F, Player] {
+
+ override def speech(player: Player, messages: Seq[FairyMessage]): F[Unit] = for {
+ fairyPlaySound <- persistence.playSoundOnFairySpeech(player.getUniqueId)
+ _ <- speechService(player).makeSpeech(messages, fairyPlaySound)
+ } yield ()
+
+ override def togglePlaySoundOnSpeech: Kleisli[F, Player, Unit] = Kleisli { player =>
+ for {
+ fairyPlaySound <- playSoundOnSpeech(player.getUniqueId)
+ _ <- persistence.setPlaySoundOnSpeech(player.getUniqueId, fairyPlaySound)
+ } yield ()
+ }
+
+ override def playSoundOnSpeech(player: UUID): F[Boolean] =
+ persistence.playSoundOnFairySpeech(player)
+ }
+ }
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/bukkit/BukkitFairySpeechGateway.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/bukkit/BukkitFairySpeechGateway.scala
new file mode 100644
index 0000000000..5cee695de1
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/bukkit/BukkitFairySpeechGateway.scala
@@ -0,0 +1,34 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.bukkit
+
+import cats.effect.{Sync, Timer}
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyMessage
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.domain.FairySpeechGateway
+import com.github.unchama.targetedeffect.commandsender.MessageEffectF
+import com.github.unchama.targetedeffect.player.FocusedSoundEffectF
+import org.bukkit.ChatColor.{AQUA, BOLD, RESET}
+import org.bukkit.Sound
+import org.bukkit.entity.Player
+
+import scala.concurrent.duration.DurationInt
+
+class BukkitFairySpeechGateway[F[_]: Sync: Timer](player: Player)
+ extends FairySpeechGateway[F] {
+
+ override def sendMessage(fairyMessages: Seq[FairyMessage]): F[Unit] = {
+ val defaultFairyMessage = s"$AQUA$BOLD<マナ妖精>$RESET%s"
+ MessageEffectF[F](
+ fairyMessages.map(input => defaultFairyMessage.format(input.message)).toList
+ ).run(player)
+ }
+
+ import cats.implicits._
+
+ override def playSpeechSound: F[Unit] = for {
+ _ <- FocusedSoundEffectF(Sound.BLOCK_NOTE_PLING, 2.0f, 1.0f).run(player)
+ _ <- Timer[F].sleep(100.millis)
+ _ <- FocusedSoundEffectF(Sound.BLOCK_NOTE_PLING, 2.0f, 1.5f).run(player)
+ _ <- Timer[F].sleep(100.millis)
+ _ <- FocusedSoundEffectF(Sound.BLOCK_NOTE_PLING, 2.0f, 2.0f).run(player)
+ } yield {}
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/domain/FairySpeechGateway.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/domain/FairySpeechGateway.scala
new file mode 100644
index 0000000000..4b960ef9ab
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/domain/FairySpeechGateway.scala
@@ -0,0 +1,17 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.domain
+
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyMessage
+
+trait FairySpeechGateway[F[_]] {
+
+ /**
+ * @return 妖精からメッセージを送信する作用
+ */
+ def sendMessage(fairyMessages: Seq[FairyMessage]): F[Unit]
+
+ /**
+ * @return 妖精がメッセージを送信した時の音を再生する作用
+ */
+ def playSpeechSound: F[Unit]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/domain/FairySpeechPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/domain/FairySpeechPersistence.scala
new file mode 100644
index 0000000000..af026b9be1
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/domain/FairySpeechPersistence.scala
@@ -0,0 +1,17 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.domain
+
+import java.util.UUID
+
+trait FairySpeechPersistence[F[_]] {
+
+ /**
+ * @return 妖精が喋るときに音を再生するかを`playOnSpeech`に変更する作用
+ */
+ def setPlaySoundOnSpeech(player: UUID, playOnSpeech: Boolean): F[Unit]
+
+ /**
+ * @return 妖精が喋ったときに音を再生するか取得する作用
+ */
+ def playSoundOnFairySpeech(player: UUID): F[Boolean]
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/infrastructure/JdbcFairySpeechPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/infrastructure/JdbcFairySpeechPersistence.scala
new file mode 100644
index 0000000000..d73acc49c7
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/infrastructure/JdbcFairySpeechPersistence.scala
@@ -0,0 +1,30 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.infrastructure
+
+import cats.effect.Sync
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.domain.FairySpeechPersistence
+import scalikejdbc.{DB, scalikejdbcSQLInterpolationImplicitDef}
+
+import java.util.UUID
+
+class JdbcFairySpeechPersistence[F[_]: Sync] extends FairySpeechPersistence[F] {
+
+ override def setPlaySoundOnSpeech(player: UUID, playOnSpeech: Boolean): F[Unit] =
+ Sync[F].delay {
+ DB.localTx { implicit session =>
+ sql"UPDATE vote_fairy SET is_play_fairy_speech_sound = $playOnSpeech WHERE uuid = ${player.toString}"
+ .execute()
+ .apply()
+ }
+ }
+
+ override def playSoundOnFairySpeech(player: UUID): F[Boolean] = Sync[F].delay {
+ DB.readOnly { implicit session =>
+ sql"SELECT is_play_fairy_speech_sound FROM vote_fairy WHERE uuid=${player.toString}"
+ .map(_.boolean("is_play_fairy_speech_sound"))
+ .single()
+ .apply()
+ .get
+ }
+ }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/service/FairySpeechService.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/service/FairySpeechService.scala
new file mode 100644
index 0000000000..927e44989c
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/vote/subsystems/fairyspeech/service/FairySpeechService.scala
@@ -0,0 +1,13 @@
+package com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.service
+
+import cats.effect.Sync
+import cats.implicits._
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairy.domain.property.FairyMessage
+import com.github.unchama.seichiassist.subsystems.vote.subsystems.fairyspeech.domain.FairySpeechGateway
+
+class FairySpeechService[F[_]: Sync](gateway: FairySpeechGateway[F]) {
+
+ def makeSpeech(fairyMessage: Seq[FairyMessage], fairyPlaySound: Boolean): F[Unit] =
+ gateway.sendMessage(fairyMessage) >> Sync[F].whenA(fairyPlaySound)(gateway.playSpeechSound)
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
index e2bf95fa3d..ef1eac42e8 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
@@ -182,13 +182,10 @@ object PlayerDataLoading {
playerData.unclaimedApologyItems = rs.getInt("numofsorryforbug")
playerData.regionCount = rs.getInt("rgnum")
playerData.playTick = rs.getLong("playtick")
- playerData.p_givenvote = rs.getInt("p_givenvote")
- playerData.effectPoint = rs.getInt("effectpoint")
playerData.totalexp = rs.getInt("totalexp")
// 実績、二つ名の情報
- playerData.p_vote_forT = rs.getInt("p_vote")
playerData.giveachvNo = rs.getInt("giveachvNo")
playerData.achievePoint = AchievementPoint(
rs.getInt("achvPointMAX"),
@@ -254,8 +251,6 @@ object PlayerDataLoading {
playerData.lastcheckdate = sdf.format(cal.getTime)
- playerData.ChainVote = rs.getInt("chainvote")
-
// 実績解除フラグのBitSet型への復元処理
// 初回nullエラー回避のための分岐
try {
@@ -273,15 +268,6 @@ object PlayerDataLoading {
playerData.TitleFlags.addOne(1)
}
- // マナ妖精
- playerData.usingVotingFairy = rs.getBoolean("canVotingFairyUse")
- playerData.VotingFairyRecoveryValue = rs.getInt("VotingFairyRecoveryValue")
- playerData.hasVotingFairyMana = rs.getInt("hasVotingFairyMana")
- playerData.toggleGiveApple = rs.getInt("toggleGiveApple")
- playerData.toggleVotingFairy = rs.getInt("toggleVotingFairy")
- playerData.setVotingFairyTime(rs.getString("newVotingFairyTime"))
- playerData.p_apple = rs.getLong("p_apple")
-
playerData.giganticBerserk = GiganticBerserk(
rs.getInt("GBlevel"),
rs.getInt("GBexp"),
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala
index 92db96d575..beeea788da 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala
@@ -123,7 +123,6 @@ object PlayerDataSaveTask {
.performMultipleIDBlockBreakWhenOutsideSeichiWorld
+ ",pvpflag = " + playerdata.settings.pvpflag
- + ",effectpoint = " + playerdata.effectPoint
+ ",totalexp = " + playerdata.totalexp
+ ",everysound = " + playerdata
.settings
@@ -150,15 +149,6 @@ object PlayerDataSaveTask {
+ ",TotalJoin = " + playerdata.loginStatus.totalLoginDay
+ ",LimitedLoginCount = " + playerdata.LimitedLoginCount
- // 投票
- + ",canVotingFairyUse = " + playerdata.usingVotingFairy
- + ",newVotingFairyTime = '" + playerdata.getVotingFairyStartTimeAsString + "'"
- + ",VotingFairyRecoveryValue = " + playerdata.VotingFairyRecoveryValue
- + ",hasVotingFairyMana = " + playerdata.hasVotingFairyMana
- + ",toggleGiveApple = " + playerdata.toggleGiveApple
- + ",toggleVotingFairy = " + playerdata.toggleVotingFairy
- + ",p_apple = " + playerdata.p_apple
-
+ ",GBstage = " + playerdata.giganticBerserk.stage
+ ",GBexp = " + playerdata.giganticBerserk.exp
+ ",GBlevel = " + playerdata.giganticBerserk.level
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/VotingFairyTask.scala b/src/main/scala/com/github/unchama/seichiassist/task/VotingFairyTask.scala
deleted file mode 100644
index 60d9eba784..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/task/VotingFairyTask.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.github.unchama.seichiassist.task
-
-import cats.effect.{IO, SyncIO}
-import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.seichiassist.listener.VotingFairyListener
-import com.github.unchama.seichiassist.subsystems.mana.ManaApi
-import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
-import com.github.unchama.seichiassist.util.TimeUtils
-import net.md_5.bungee.api.ChatColor
-import org.bukkit.ChatColor._
-import org.bukkit.entity.Player
-import org.bukkit.inventory.ItemStack
-import org.bukkit.{Bukkit, Sound}
-
-object VotingFairyTask {
- // MinuteTaskRunnableから、妖精召喚中のプレイヤーを対象に毎分実行される
- def run(p: Player)(
- implicit manaApi: ManaApi[IO, SyncIO, Player],
- mineStackAPI: MineStackAPI[IO, Player, ItemStack]
- ): Unit = {
- val playermap = SeichiAssist.playermap
- val uuid = p.getUniqueId
- val playerdata = playermap.apply(uuid)
- // マナ回復
- VotingFairyListener.regeneMana(p)
- // 効果時間中か
- if (
- !TimeUtils.isVotingFairyPeriod(
- playerdata.votingFairyStartTime,
- playerdata.votingFairyEndTime
- )
- ) {
- speak(p, "あっ、もうこんな時間だ!", b = false)
- speak(p, s"じゃーねー!${p.getName}", b = true)
- p.sendMessage(s"$RESET$YELLOW${BOLD}妖精はどこかへ行ってしまった")
- playerdata.usingVotingFairy_$eq(false)
- }
- }
-
- def speak(p: Player, msg: String, b: Boolean): Unit = {
- if (b) playSe(p)
- p.sendMessage(s"${ChatColor.AQUA}${ChatColor.BOLD}<マナ妖精>${ChatColor.RESET}$msg")
- }
-
- // 妖精効果音
- private def playSe(p: Player): Unit = {
- p.playSound(p.getLocation, Sound.BLOCK_NOTE_PLING, 2.0f, 1.0f)
- Bukkit
- .getServer
- .getScheduler
- .runTaskLater(
- SeichiAssist.instance,
- (() => {
- p.playSound(p.getLocation, Sound.BLOCK_NOTE_PLING, 2.0f, 1.5f)
- Bukkit
- .getServer
- .getScheduler
- .runTaskLater(
- SeichiAssist.instance,
- (() => p.playSound(p.getLocation, Sound.BLOCK_NOTE_PLING, 2.0f, 2.0f)): Runnable,
- 2
- )
- }): Runnable,
- 2
- )
- }
-
- def dispToggleVFTime(toggle: Int): String = {
- if (toggle == 1) "30分"
- else if (toggle == 2) "1時間"
- else if (toggle == 3) "1時間30分"
- else if (toggle == 4) "2時間"
- else "エラー"
- }
-}
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataBackupRoutine.scala b/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataBackupRoutine.scala
index 7edbb40a2f..957646237c 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataBackupRoutine.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataBackupRoutine.scala
@@ -45,19 +45,11 @@ object PlayerDataBackupRoutine {
} yield ()
}
- val updateRankingData = IO {
- // ランキングリストを最新情報に更新する
- if (!SeichiAssist.databaseGateway.playerDataManipulator.successRankingUpdate()) {
- SeichiAssist.instance.getLogger.info("ランキングデータの作成に失敗しました")
- }
- }
-
for {
saveRequired <- IO {
SeichiAssist.playermap.nonEmpty
}
_ <- if (saveRequired) save else IO.unit
- _ <- updateRankingData
} yield true
}
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataRecalculationRoutine.scala b/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataRecalculationRoutine.scala
index dd5246bad9..b7ed284b9f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataRecalculationRoutine.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/global/PlayerDataRecalculationRoutine.scala
@@ -5,12 +5,7 @@ import com.github.unchama.concurrent.{RepeatingRoutine, RepeatingTaskContext}
import com.github.unchama.minecraft.actions.OnMinecraftServerThread
import com.github.unchama.seichiassist.SeichiAssist
import com.github.unchama.seichiassist.achievement.SeichiAchievement
-import com.github.unchama.seichiassist.subsystems.mana.ManaApi
-import com.github.unchama.seichiassist.subsystems.minestack.MineStackAPI
-import com.github.unchama.seichiassist.task.VotingFairyTask
import org.bukkit.Bukkit
-import org.bukkit.entity.Player
-import org.bukkit.inventory.ItemStack
import scala.concurrent.duration.FiniteDuration
@@ -18,9 +13,7 @@ object PlayerDataRecalculationRoutine {
def apply()(
implicit onMainThread: OnMinecraftServerThread[IO],
- context: RepeatingTaskContext,
- manaApi: ManaApi[IO, SyncIO, Player],
- mineStackAPI: MineStackAPI[IO, Player, ItemStack]
+ context: RepeatingTaskContext
): IO[Nothing] = {
val getRepeatInterval: IO[FiniteDuration] = IO {
import scala.concurrent.duration._
@@ -66,11 +59,6 @@ object PlayerDataRecalculationRoutine {
)
.unsafeRunSync()
- // 投票妖精関連
- if (playerData.usingVotingFairy) {
- VotingFairyTask.run(player)
- }
-
// GiganticBerserk
playerData.GBcd = 0
diff --git a/src/main/scala/com/github/unchama/seichiassist/util/BreakUtil.scala b/src/main/scala/com/github/unchama/seichiassist/util/BreakUtil.scala
index 3f62687dfd..8368a43bd6 100644
--- a/src/main/scala/com/github/unchama/seichiassist/util/BreakUtil.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/util/BreakUtil.scala
@@ -299,6 +299,13 @@ object BreakUtil {
Some(BlockBreakResult.SpawnSilverFish(blockLocation))
case Material.LOG | Material.LOG_2 =>
Some(BlockBreakResult.ItemDrop(new ItemStack(blockMaterial, 1, b_tree.toShort)))
+ case Material.WOOD_STEP | Material.STEP | Material.STONE_SLAB2
+ if (blockDataLeast4Bits & 8) != 0 =>
+ // 上付きハーフブロックをそのままドロップするとmissing textureとして描画されるため、下付きの扱いとする
+ Some(
+ BlockBreakResult
+ .ItemDrop(new ItemStack(blockMaterial, 1, (blockDataLeast4Bits & 7).toShort))
+ )
case _ =>
Some(
BlockBreakResult
diff --git a/src/main/scala/com/github/unchama/seichiassist/util/TimeUtils.scala b/src/main/scala/com/github/unchama/seichiassist/util/TimeUtils.scala
deleted file mode 100644
index b159dd3d4f..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/util/TimeUtils.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.github.unchama.seichiassist.util
-
-import java.text.SimpleDateFormat
-import java.util.Calendar
-
-object TimeUtils {
- def showTime(cal: Calendar): String = {
- val date = cal.getTime
- val format = new SimpleDateFormat("yyyy/MM/dd HH:mm")
- format.format(date)
- }
-
- def showHour(cal: Calendar): String = {
- val date = cal.getTime
- val format = new SimpleDateFormat("HH:mm")
- format.format(date)
- }
-
- def getTimeZone(cal: Calendar): String = {
- val date = cal.getTime
- val format = new SimpleDateFormat("HH")
- val n = Integer.parseInt(format.format(date))
- if (4 <= n && n < 10)
- "morning"
- else if (10 <= n && n < 18)
- "day"
- else
- "night"
- }
-
- def isVotingFairyPeriod(start: Calendar, end: Calendar): Boolean = {
- val cur = Calendar.getInstance()
- cur.after(start) && cur.before(end)
- }
-}
diff --git a/src/main/scala/com/github/unchama/targetedeffect/package.scala b/src/main/scala/com/github/unchama/targetedeffect/package.scala
index 34acaf4784..4fbcbc00e0 100644
--- a/src/main/scala/com/github/unchama/targetedeffect/package.scala
+++ b/src/main/scala/com/github/unchama/targetedeffect/package.scala
@@ -6,5 +6,6 @@ import cats.effect.IO
package object targetedeffect {
type TargetedEffect[-T] = Kleisli[IO, T, Unit]
+ type TargetedEffectF[F[_], -T] = Kleisli[F, T, Unit]
}
diff --git a/src/main/scala/com/github/unchama/targetedeffect/player/FocusedSoundEffect.scala b/src/main/scala/com/github/unchama/targetedeffect/player/FocusedSoundEffect.scala
index 6cd25c03b6..5a09b43faa 100644
--- a/src/main/scala/com/github/unchama/targetedeffect/player/FocusedSoundEffect.scala
+++ b/src/main/scala/com/github/unchama/targetedeffect/player/FocusedSoundEffect.scala
@@ -1,6 +1,7 @@
package com.github.unchama.targetedeffect.player
-import com.github.unchama.targetedeffect.TargetedEffect
+import cats.effect.Sync
+import com.github.unchama.targetedeffect.{TargetedEffect, TargetedEffectF}
import org.bukkit.Sound
import org.bukkit.entity.Player
@@ -14,3 +15,11 @@ object FocusedSoundEffect {
player.playSound(player.getLocation, sound, volume, pitch)
}
}
+
+object FocusedSoundEffectF {
+ def apply[F[_]: Sync](sound: Sound, volume: Float, pitch: Float): TargetedEffectF[F, Player] =
+ TargetedEffect.delay[F, Player] { player =>
+ // 音を鳴らすのは非同期スレッドでも問題ない(Spigot 1.12.2)
+ player.playSound(player.getLocation, sound, volume, pitch)
+ }
+}