From e8614a5945abe84a84f848677a002eabdd9b3a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrik=20Laur=C3=A9n?= Date: Fri, 21 Jan 2022 01:10:17 +0100 Subject: [PATCH] adding clickable monsters in the hex crawling table text --- app/src/main/assets/monsters/goblins.json | 2 +- app/src/main/assets/monsters/orcs.json | 2 +- app/src/main/assets/monsters/trolls.json | 2 +- .../ruinmasters/DisplayResultFragment.kt | 85 --------- .../ruinmasters/DisplayTableResultFragment.kt | 174 ++++++++++++++++++ .../my/tablelogic/ruinmasters/MainActivity.kt | 44 ++++- .../tablelogic/ruinmasters/TablesFragment.kt | 8 +- 7 files changed, 222 insertions(+), 95 deletions(-) delete mode 100644 app/src/main/java/my/tablelogic/ruinmasters/DisplayResultFragment.kt create mode 100644 app/src/main/java/my/tablelogic/ruinmasters/DisplayTableResultFragment.kt diff --git a/app/src/main/assets/monsters/goblins.json b/app/src/main/assets/monsters/goblins.json index 6cb7bbb..039226e 100644 --- a/app/src/main/assets/monsters/goblins.json +++ b/app/src/main/assets/monsters/goblins.json @@ -2,7 +2,7 @@ "monster":[ { "id":1, "name":"Goblin", - "tags": [ "Goblin", "goblin", "Goblins", "goblins" ], + "tags": [ "goblin", "goblins" ], "stats": { "traits": { "phy": "12", "min": "10", "int": "10", "cha": "10" }, "skills": { "bur": 69, "kno": 17, "mag": 15, "mel": 49, "soc": 38, "sur": 58 }, diff --git a/app/src/main/assets/monsters/orcs.json b/app/src/main/assets/monsters/orcs.json index 1270c89..f37fc69 100644 --- a/app/src/main/assets/monsters/orcs.json +++ b/app/src/main/assets/monsters/orcs.json @@ -2,7 +2,7 @@ "monster":[ { "id":1, "name":"Orc", - "tags": [ "Orc", "orc", "Orcs", "orcs" ], + "tags": [ "orc", "orcs" ], "stats": { "traits": { "phy": "30", "min": "15", "int": "15", "cha": "5" }, "skills": { "bur": 39, "kno": 27, "mag": 25, "mel": 60, "soc": 28, "sur": 52 }, diff --git a/app/src/main/assets/monsters/trolls.json b/app/src/main/assets/monsters/trolls.json index 53480a2..bdb91cb 100644 --- a/app/src/main/assets/monsters/trolls.json +++ b/app/src/main/assets/monsters/trolls.json @@ -2,7 +2,7 @@ "monster":[ { "id":1, "name":"Troll", - "tags": [ "Troll", "troll", "Trolls", "trolls" ], + "tags": [ "troll", "trolls" ], "stats": { "traits": { "phy": "30", "min": "15", "int": "10", "cha": "5" }, "skills": { "bur": 48, "kno": 29, "mag": 27, "mel": 63, "soc": 18, "sur": 43 }, diff --git a/app/src/main/java/my/tablelogic/ruinmasters/DisplayResultFragment.kt b/app/src/main/java/my/tablelogic/ruinmasters/DisplayResultFragment.kt deleted file mode 100644 index 73c07b7..0000000 --- a/app/src/main/java/my/tablelogic/ruinmasters/DisplayResultFragment.kt +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2021 Ulrik Laurén -// Part of RuinMastersTables -// MIT License, see LICENSE file - -package my.tablelogic.ruinmasters - -import android.content.res.Resources -import android.graphics.Rect -import android.graphics.Typeface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment -import android.widget.TextView -import androidx.appcompat.widget.AppCompatButton - -private const val ARG_HEADER = "ARG_HEADER" -private const val ARG_TERRAIN = "ARG_TERRAIN" -private const val ARG_ENCOUNTER = "ARG_ENCOUNTER" -private const val ARG_TREASURE = "ARG_TREASURE" - -class DisplayResultFragment : DialogFragment() { - private var headerText: String? = null - private var terrainText: String? = null - private var encounterText: String? = null - private var treasureText: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - headerText = it.getString(ARG_HEADER) - terrainText = it.getString(ARG_TERRAIN) - encounterText = it.getString(ARG_ENCOUNTER) - treasureText = it.getString(ARG_TREASURE) - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - // Inflate the layout for this fragment - val v: View = inflater.inflate(R.layout.fragment_display_result, container, false) - val header = v.findViewById(R.id.tvHeaderText) - val terrain = v.findViewById(R.id.tvTerrainText) - val encounter = v.findViewById(R.id.tvEncounterText) - val treasure = v.findViewById(R.id.tvTreasureText) - val dismissButton = v.findViewById(R.id.btnDismiss) - - header.text = arguments?.getString(ARG_HEADER) - terrain.text = arguments?.getString(ARG_TERRAIN) - encounter.text = arguments?.getString(ARG_ENCOUNTER) - treasure.text = arguments?.getString(ARG_TREASURE) - dismissButton.setOnClickListener { dismiss() } - - return v - } - - override fun onResume() { - super.onResume() - setWidthPercent(95) - } - - private fun setWidthPercent(percentage: Int) { - val percent = percentage.toFloat() / 100 - val dm = Resources.getSystem().displayMetrics - val rect = dm.run { Rect(0, 0, widthPixels, heightPixels) } - val percentWidth = rect.width() * percent - dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) - } - - companion object { - @JvmStatic - fun newInstance(headerText: String, terrainText: String, encounterText: String, treasureText: String) = - DisplayResultFragment().apply { - arguments = Bundle().apply { - putString(ARG_HEADER, headerText) - putString(ARG_TERRAIN, terrainText) - putString(ARG_ENCOUNTER, encounterText) - putString(ARG_TREASURE, treasureText) - } - } - } -} diff --git a/app/src/main/java/my/tablelogic/ruinmasters/DisplayTableResultFragment.kt b/app/src/main/java/my/tablelogic/ruinmasters/DisplayTableResultFragment.kt new file mode 100644 index 0000000..afa3b21 --- /dev/null +++ b/app/src/main/java/my/tablelogic/ruinmasters/DisplayTableResultFragment.kt @@ -0,0 +1,174 @@ +// Copyright (c) 2021 Ulrik Laurén +// Part of RuinMastersTables +// MIT License, see LICENSE file + +package my.tablelogic.ruinmasters + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.os.Bundle +import android.text.Spanned +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import android.widget.TextView +import androidx.appcompat.widget.AppCompatButton +import androidx.core.content.ContextCompat +import androidx.core.text.toSpannable +import java.util.regex.Pattern + +private const val ARG_HEADER = "ARG_HEADER" +private const val ARG_TERRAIN = "ARG_TERRAIN" +private const val ARG_ENCOUNTER = "ARG_ENCOUNTER" +private const val ARG_TREASURE = "ARG_TREASURE" + +class DisplayTableResultFragment : DialogFragment() { + private var headerText: String? = null + private var terrainText: String? = null + private var encounterText: String? = null + private var treasureText: String? = null + private lateinit var myView: View + private lateinit var myContext: Context + private lateinit var monsterTagIdMap : Map + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + headerText = it.getString(ARG_HEADER) + terrainText = it.getString(ARG_TERRAIN) + encounterText = it.getString(ARG_ENCOUNTER) + treasureText = it.getString(ARG_TREASURE) + } + monsterTagIdMap = (activity as MainActivity).getMonsterTagMap() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + myContext = context + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // Inflate the layout for this fragment + myView = inflater.inflate(R.layout.fragment_display_result, container, false) + val header = myView.findViewById(R.id.tvHeaderText) + val terrain = myView.findViewById(R.id.tvTerrainText) + val encounter = myView.findViewById(R.id.tvEncounterText) + val treasure = myView.findViewById(R.id.tvTreasureText) + val dismissButton = myView.findViewById(R.id.btnDismiss) + + header.text = arguments?.getString(ARG_HEADER) + terrain.text = arguments?.getString(ARG_TERRAIN) + encounter.text = arguments?.getString(ARG_ENCOUNTER) + treasure.text = arguments?.getString(ARG_TREASURE) + + setClickableTags(terrain) + setClickableTags(encounter) + + dismissButton.setOnClickListener { dismiss() } + + return myView + } + + override fun onResume() { + super.onResume() + setWidthPercent(95) + } + + private fun countMatches(string: String, pattern: String): Int { + return string.split(pattern, ignoreCase = true) + .dropLastWhile { it.isEmpty() } + .toTypedArray().size - 1 + } + + private fun setClickableTags(clickableTextView: TextView) { + val str = clickableTextView.text.toSpannable() + + debug("Parsing '${str}'") + + // Go through the tags in length order to resolve the 'giant wolf' vs 'wolf' problem + val foundTags = (monsterTagIdMap.keys.filter { str.contains(it, ignoreCase = true) }).sortedByDescending { it.length } + if (foundTags.isNotEmpty()) { + foundTags.forEach { tag -> + val monsterId = monsterTagIdMap.getOrDefault(tag, -1) + debug("Found tag=$tag with id=$monsterId ${countMatches(str.toString(), tag)} times.") + + val matcher = Pattern.compile(tag, Pattern.CASE_INSENSITIVE).matcher(str) + + while (matcher.find()) { + val matchStart = matcher.start(0) + val matchEnd = matcher.end() + val clickableSpan: ClickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + Log.d("RuinMastersTables::DisplayTableResultFragment", "Clicked on $tag.") + if (monsterId > 0) { + val monster = (activity as MainActivity).getMonster(monsterId) + if (monster != null) { + debug("Show monster with id=${monster.id} and name='${monster.name}'") + val displayMonsterFragment: DisplayMonsterFragment = DisplayMonsterFragment.newInstance(monster) + displayMonsterFragment.show(requireActivity().supportFragmentManager, "fragment_display_monster") + } else { + Log.d("RuinMastersTables::DisplayTableResultFragment","Failed to get monster.") + } + } else { + warning("Invalid monster Id=monsterId") + } + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = ContextCompat.getColor(myContext, R.color.rm_text_dark)//Color.parseColor("#689899") + ds.isFakeBoldText = true + ds.isUnderlineText = false // set to false to remove underline + } + } + debug("Set span for tag=$tag starting at $matchStart and ending at $matchEnd.") + str.setSpan(clickableSpan, matchStart, matchEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } else { + debug("No tags found in text.") + } + + // Make the text view text clickable + clickableTextView.movementMethod = LinkMovementMethod() + clickableTextView.text = str + } + + private fun setWidthPercent(percentage: Int) { + val percent = percentage.toFloat() / 100 + val dm = Resources.getSystem().displayMetrics + val rect = dm.run { Rect(0, 0, widthPixels, heightPixels) } + val percentWidth = rect.width() * percent + dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) + } + + private fun warning(message: String) { + if (BuildConfig.DEBUG) Log.w("RuinMastersTables::DisplayTableResultFragment", message) + } + + private fun debug(message: String) { + if (BuildConfig.DEBUG) Log.d("RuinMastersTables::DisplayTableResultFragment", message) + } + + companion object { + @JvmStatic + fun newInstance(headerText: String, terrainText: String, encounterText: String, treasureText: String) = + DisplayTableResultFragment().apply { + arguments = Bundle().apply { + putString(ARG_HEADER, headerText) + putString(ARG_TERRAIN, terrainText) + putString(ARG_ENCOUNTER, encounterText) + putString(ARG_TREASURE, treasureText) + } + } + } +} diff --git a/app/src/main/java/my/tablelogic/ruinmasters/MainActivity.kt b/app/src/main/java/my/tablelogic/ruinmasters/MainActivity.kt index a8a0d16..59d79dc 100644 --- a/app/src/main/java/my/tablelogic/ruinmasters/MainActivity.kt +++ b/app/src/main/java/my/tablelogic/ruinmasters/MainActivity.kt @@ -225,6 +225,8 @@ class MainActivity : AppCompatActivity() { return Pair(loadedMonsterData, loadedMonsterFiles) } + private fun isInteger(input: String) = input.all { it in '0'..'9' } + private fun isMonsterDataValid(monsterData : MonsterData, monsterDataFiles: ArrayList) { val allIds = ArrayList() val allTags = ArrayList() @@ -268,6 +270,15 @@ class MainActivity : AppCompatActivity() { if (monster.abilities.size > 10) { error("To many abilities defined (${monster.combat.attacks.size}) used by id=${getActualMonsterId(monster.id)} in ${getMonsterFileName(monster.id, monsterDataFiles)}") } + + if (monster.tags.isNotEmpty()) { + if (!isInteger(monster.stats.traits.phy) || + !isInteger(monster.stats.traits.min) || + !isInteger(monster.stats.traits.int) || + !isInteger(monster.stats.traits.cha)) { + error("Tags are only supported for monsters not using random values, issue with id=${getActualMonsterId(monster.id)} in ${getMonsterFileName(monster.id, monsterDataFiles)}!") + } + } } if (allIds.size != allIds.distinct().count()) { val duplicatedIds = allIds.groupingBy { it }.eachCount().filter { it.value > 1 } @@ -288,15 +299,25 @@ class MainActivity : AppCompatActivity() { } private fun createMonsterTagMap(monsterData : MonsterData) : Map{ - val idTagMap = emptyMap().toMutableMap() + val tagIdMap = emptyMap().toMutableMap() monsterData.monster.forEach { monster -> if (monster.tags.isNotEmpty()) { monster.tags.forEach { tag -> - idTagMap += mapOf(Pair(tag, monster.id)) + if (!tagIdMap.containsKey(tag)) { + tagIdMap += mapOf(Pair(tag, monster.id)) + } else { + error("Already found tag=$tag in the tagIdMap! Duplicates not allowed") + val existingId = tagIdMap.getOrDefault(tag,-1) + if (existingId > 0) { + error("Collision between id=${getActualMonsterId(existingId)} in ${getMonsterFileName(existingId,monsters.second)}" + + " and id=${getActualMonsterId(monster.id)} in ${getMonsterFileName(monster.id,monsters.second)}") + } + } } + tagIdMap.toSortedMap() } } - return idTagMap.toSortedMap() + return tagIdMap } private fun getActualMonsterId(monsterId : Int) : Int { @@ -342,6 +363,23 @@ class MainActivity : AppCompatActivity() { } } + fun getMonster(id: Int) : Monster? { + debug("Try to get monster with id=$id.") + monsters.first.monster.forEach { monster -> + if (monster.id == id) { + return monster.deepCopy() + } + } + error("Did not find id='$id'in monsterData.") + return null + } + + fun getMonsterTagMap() : Map { + debug("Get monsterTagMap, it has ${monsterTagMap.size} number of entries.") + + return monsterTagMap + } + private fun error(message: String) { Log.e("RuinMastersTables::MainActivity", message) } diff --git a/app/src/main/java/my/tablelogic/ruinmasters/TablesFragment.kt b/app/src/main/java/my/tablelogic/ruinmasters/TablesFragment.kt index 851ceab..43f0345 100644 --- a/app/src/main/java/my/tablelogic/ruinmasters/TablesFragment.kt +++ b/app/src/main/java/my/tablelogic/ruinmasters/TablesFragment.kt @@ -78,7 +78,7 @@ class TablesFragment(tables: TableData, files: ArrayList) : Fragment(), treasureText = treasureText.trimStart() if (treasureText.isNotBlank()) treasureText = replaceDieRolls(treasureText) - showEditDialog((v as Button).text.toString(), terrainText, encounterText, treasureText) + showTableResultDialog((v as Button).text.toString(), terrainText, encounterText, treasureText) } } @@ -205,9 +205,9 @@ class TablesFragment(tables: TableData, files: ArrayList) : Fragment(), return localText } - private fun showEditDialog(headerText: String, terrainText: String, encounterText: String, treasureText: String) { - val displayResultFragment: DisplayResultFragment = DisplayResultFragment.newInstance(headerText, terrainText, encounterText, treasureText) - displayResultFragment.show(requireActivity().supportFragmentManager, "fragment_display_result") + private fun showTableResultDialog(headerText: String, terrainText: String, encounterText: String, treasureText: String) { + val displayResultResultFragment: DisplayTableResultFragment = DisplayTableResultFragment.newInstance(headerText, terrainText, encounterText, treasureText) + displayResultResultFragment.show(requireActivity().supportFragmentManager, "fragment_display_result") } private fun error(message: String) {