From db670f33402ed9ee2c78fbf0f5c4f1cf1b19569f Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Tue, 1 Sep 2020 23:42:09 -0700 Subject: [PATCH] Adds abilities to monster cards. Adds CommonMark dependency and CommonMarkHelper to render it to html. --- app/build.gradle | 7 +- .../helpers/CommonMarkHelper.java | 38 +++++++++++ .../monstercards/models/Ability.java | 25 +++++++ .../monstercards/models/Monster.java | 68 ++++++++++++++++++- .../ui/monster/MonsterFragment.java | 44 +++++++++++- .../ui/monster/MonsterViewModel.java | 10 +++ app/src/main/res/layout/fragment_monster.xml | 15 ++++ 7 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/models/Ability.java diff --git a/app/build.gradle b/app/build.gradle index 7683791..aa9785d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,13 +13,13 @@ apply plugin: 'com.android.application' apply plugin: "androidx.navigation.safeargs" android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion 30 + buildToolsVersion '30.0.2' defaultConfig { applicationId "com.majinnaibu.monstercards" minSdkVersion 22 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\"" @@ -56,6 +56,7 @@ dependencies { implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'com.atlassian.commonmark:commonmark:0.15.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java b/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java new file mode 100644 index 0000000..bc459da --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java @@ -0,0 +1,38 @@ +package com.majinnaibu.monstercards.helpers; + +import org.commonmark.node.Document; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.NodeRenderer; +import org.commonmark.renderer.html.HtmlNodeRendererContext; +import org.commonmark.renderer.html.HtmlNodeRendererFactory; +import org.commonmark.renderer.html.HtmlRenderer; + +public final class CommonMarkHelper { + private static final class MyNodeRendererFactory implements HtmlNodeRendererFactory { + + @Override + public NodeRenderer create(HtmlNodeRendererContext context) { + return null; + } + } + + public static String toHtml(String rawCommonMark) { + Parser parser = Parser.builder().build(); + Node document = parser.parse(rawCommonMark); + Node parent1 = document.getFirstChild(); + Node parent2 = document.getLastChild(); + if (parent1 == parent2 && parent1 instanceof Paragraph) { + document = new Document(); + Node child = parent1.getFirstChild(); + while(child != null) { + Node nextChild = child.getNext(); + document.appendChild(child); + child = nextChild;//child.getNext(); + } + } + HtmlRenderer renderer = HtmlRenderer.builder().build(); + return renderer.render(document); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Ability.java b/app/src/main/java/com/majinnaibu/monstercards/models/Ability.java new file mode 100644 index 0000000..feb2efe --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Ability.java @@ -0,0 +1,25 @@ +package com.majinnaibu.monstercards.models; + +public class Ability { + + public Ability(String name, String description) { + mName = name; + mDescription = description; + } + + private String mName; + public String getName() { + return mName; + } + public void setName(String name) { + mName = name; + } + + private String mDescription; + public String getDescription() { + return mDescription; + } + public void setDescription(String description) { + mDescription = description; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java b/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java index 2cf878b..61aff23 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java @@ -13,11 +13,12 @@ import java.util.Set; public class Monster { public Monster() { + mAbilities = new ArrayList<>(); + mConditionImmunities = new HashSet<>(); + mDamageTypes = new HashSet<>(); + mLanguages = new HashSet<>(); mSavingThrows = new HashSet<>(); mSkills = new HashSet<>(); - mDamageTypes = new HashSet<>(); - mConditionImmunities = new HashSet<>(); - mLanguages = new HashSet<>(); } private String mName; @@ -917,4 +918,65 @@ public class Monster { return getCustomChallengeRating(); } } + + private ArrayList mAbilities; + public List getAbilities() { + return mAbilities; + } + public void addAbility(Ability ability) { + mAbilities.add(ability); + } + public void removeAbility(Ability ability) { + mAbilities.remove(ability); + } + public void clearAbilities() { + mAbilities.clear(); + } + + public List getAbilityDescriptions() { + ArrayList abilities = new ArrayList<>(); + for (Ability ability : getAbilities()) { + abilities.add(getPlaceholderReplacedText(String.format("__%s__ %s", ability.getName(), ability.getDescription()))); + } + return abilities; + } + + public String getPlaceholderReplacedText(String rawText) { + return rawText + .replaceAll("\\[STR SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("strength"))) + .replaceAll("\\[STR ATK]", String.format(Locale.US, "%+d", getAttackBonus("strength"))) + .replaceAll("\\[DEX SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("dexterity"))) + .replaceAll("\\[DEX ATK]", String.format(Locale.US, "%+d", getAttackBonus("dexterity"))) + .replaceAll("\\[CON SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("constitution"))) + .replaceAll("\\[CON ATK]", String.format(Locale.US, "%+d", getAttackBonus("constitution"))) + .replaceAll("\\[INT SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("intelligence"))) + .replaceAll("\\[INT ATK]", String.format(Locale.US, "%+d", getAttackBonus("intelligence"))) + .replaceAll("\\[WIS SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("wisdom"))) + .replaceAll("\\[WIS ATK]", String.format(Locale.US, "%+d", getAttackBonus("wisdom"))) + .replaceAll("\\[CHA SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("charisma"))) + .replaceAll("\\[CHA ATK]", String.format(Locale.US, "%+d", getAttackBonus("charisma"))); + } + + public int getSavingThrow(String name) { + Set sts = getSavingThrows(); + for(SavingThrow st : sts) { + if (name.equals(st.getName())) { + return getAbilityModifier(name) + getProficiencyBonus(); + } + } + return getAbilityModifier(name); + } + + public String getWisdomSave() { + return String.format(Locale.US, "%+d", getSavingThrow("wis")); + } + + public int getSpellSaveDC(String abilityScoreName) { + return 8 + getProficiencyBonus() + getAbilityModifier(abilityScoreName); + } + + public int getAttackBonus(String abilityScoreName) { + return getProficiencyBonus() + getAbilityModifier(abilityScoreName); + } + } diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterFragment.java index af427f8..2b8b1ee 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterFragment.java +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterFragment.java @@ -1,10 +1,16 @@ package com.majinnaibu.monstercards.ui.monster; +import android.content.Context; +import android.content.res.Resources; import android.os.Bundle; import android.text.Html; +import android.text.Spanned; +import android.util.DisplayMetrics; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; @@ -14,13 +20,17 @@ import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.helpers.CommonMarkHelper; import com.majinnaibu.monstercards.helpers.StringHelper; +import com.majinnaibu.monstercards.models.Ability; import com.majinnaibu.monstercards.models.DamageType; import com.majinnaibu.monstercards.models.Language; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.SavingThrow; import com.majinnaibu.monstercards.models.Skill; +import java.util.List; + @SuppressWarnings("FieldCanBeLocal") public class MonsterFragment extends Fragment { @@ -99,11 +109,14 @@ public class MonsterFragment extends Fragment { monster.addLanguage(new Language("French", true)); monster.addLanguage(new Language("Mermataur", false)); monster.addLanguage(new Language("Goldfish", false)); - // Challenge Rating monster.setChallengeRating("*"); monster.setCustomChallengeRating("Infinite (0XP)"); monster.setCustomProficiencyBonus(4); + // Abilities + monster.addAbility(new Ability("Spellcasting", "The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC [WIS SAVE], [WIS ATK] to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n\n> Cantrips (at will): _light, sacred flame, thaumaturgy_\n> 1st level (3 slots): _bless, cure wounds, sanctuary_")); + monster.addAbility(new Ability("Amphibious", "The dragon can breathe air and water.")); + monster.addAbility(new Ability("Legendary Resistance (3/Day)", "If the dragon fails a saving throw, it can choose to succeed instead.")); // END remove block monsterViewModel = new ViewModelProvider(this).get(MonsterViewModel.class); View root = inflater.inflate(R.layout.fragment_monster, container, false); @@ -309,6 +322,35 @@ public class MonsterFragment extends Fragment { } }); + final LinearLayout monsterAbilities = root.findViewById(R.id.abilities); + monsterViewModel.getAbilities().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List abilities) { + Context context = getContext(); + DisplayMetrics displayMetrics = null; + if (context != null) { + Resources resources = context.getResources(); + if (resources != null) { + displayMetrics = resources.getDisplayMetrics(); + } + } + monsterAbilities.removeAllViews(); + if (abilities != null) { + for (String ability : abilities) { + TextView tvAbility = new TextView(context); + // TODO: Handle multiline block quotes specially so they stay multiline. + // TODO: Replace QuoteSpans in the result of fromHtml with something like this https://stackoverflow.com/questions/7717567/how-to-style-blockquotes-in-android-textviews to make them indent as expected + Spanned spannedText = Html.fromHtml(CommonMarkHelper.toHtml(ability)); + tvAbility.setText(spannedText); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, displayMetrics); + tvAbility.setLayoutParams(layoutParams); + monsterAbilities.addView(tvAbility); + } + } + } + }); + return root; } } diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterViewModel.java index f0c5486..2d28237 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterViewModel.java +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterViewModel.java @@ -6,6 +6,9 @@ import androidx.lifecycle.ViewModel; import com.majinnaibu.monstercards.models.Monster; +import java.util.ArrayList; +import java.util.List; + public class MonsterViewModel extends ViewModel { public MonsterViewModel() { @@ -50,6 +53,8 @@ public class MonsterViewModel extends ViewModel { mLanguages.setValue(""); mChallenge = new MutableLiveData<>(); mChallenge.setValue(""); + mAbilities = new MutableLiveData<>(); + mAbilities.setValue(new ArrayList()); } private MutableLiveData mName; @@ -132,6 +137,10 @@ public class MonsterViewModel extends ViewModel { public LiveData getChallenge() { return mChallenge; } + private MutableLiveData> mAbilities; + public LiveData> getAbilities() { + return mAbilities; + } private Monster mMonster; public void setMonster(Monster monster) { @@ -156,5 +165,6 @@ public class MonsterViewModel extends ViewModel { mSenses.setValue(monster.getSensesDescription()); mLanguages.setValue(mMonster.getLanguagesDescription()); mChallenge.setValue(mMonster.getChallengeRatingDescription()); + mAbilities.setValue(mMonster.getAbilityDescriptions()); } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_monster.xml b/app/src/main/res/layout/fragment_monster.xml index d83aafe..7aa2002 100644 --- a/app/src/main/res/layout/fragment_monster.xml +++ b/app/src/main/res/layout/fragment_monster.xml @@ -422,5 +422,20 @@ app:layout_constraintTop_toBottomOf="@+id/languages" tools:text="Challenge" /> + + + \ No newline at end of file